Initial commit
authorrohe0002 <rohe0002@Rolands-MacBook.local>
Mon, 24 Oct 2011 13:05:53 +0000 (15:05 +0200)
committerrohe0002 <rohe0002@Rolands-MacBook.local>
Mon, 24 Oct 2011 13:05:53 +0000 (15:05 +0200)
README [new file with mode: 0644]
freeradius_ecp.py [new file with mode: 0644]
freeradius_pysaml2.py [new file with mode: 0755]
radiusd.py [new file with mode: 0644]
setup.py [new file with mode: 0644]

diff --git a/README b/README
new file mode 100644 (file)
index 0000000..97e4b6f
--- /dev/null
+++ b/README
@@ -0,0 +1,78 @@
+moonshot
+========
+A python module usable in a Moonshot environment to allow a freeradius server
+to fetch information about a user from a SAML2 Attribute Authority.
+
+Installing
+----------
+
+sudo python setup.py install
+
+eventually also
+
+sudo easy_install moonshot
+
+When the python module is installed a couple of changes to the freeradius
+configuration are necessary.
+
+1) create raddb/modules/python
+
+You can use the provided '/usr/local/etc/moonshot/template/modules_python' file
+as is.
+
+2) Edit raddb/sites-available/default
+To the 'post-auth' section add one line referencing the python module.
+You can see how it can be done in
+'/usr/local/etc/moonshot/template/sites-available_default".
+
+3) Edit raddb/sites-available/inner-tunnel.
+To the 'post-auth' section add one line referencing the python module.
+You can see how it can be done in
+'/usr/local/etc/moonshot/template/sites-available_inner-tunnel".
+
+
+Now, you should have the basic setup.
+To get it working you have to do a couple of more things:
+
+
+I) Get the SAML2 metadata for the Attribute Authority (AA) you want to use.
+
+Place it in the '/usr/local/etc/moonshot/' directory
+
+
+II) Change the configuration in /usr/local/etc/moonshot/config.py
+
+You must change the value of ATTRIBUTE_AUTHORITY so it is the identifier of the
+SAML2 AA you want to use.
+
+
+III) Change the configuration '/usr/local/etc/moonshot/pysaml_config.py'.
+A couple of things:
+
+BASE : This is the identifier of the SP (=this module) you are running.
+
+organization: Information about the organization running this service
+
+contact_person: Information about a person people can contact to ask about
+    this service
+
+
+IV) Create your own key pair.
+
+A key and certificate can be created using the openssl tool:
+$ openssl genrsa 1024 > ssl.key
+$ openssl req -new -x509 -nodes -sha1 -days 365 -key ssl.key > ssl.cert
+$ sudo mv ssl.key ssl.cert /usr/local/etc/moonshot/pki
+
+If you chose other names for you key and cert you have to change accordingly
+in pysaml_config.py .
+
+
+V) Create the metadata file for your SP.
+
+$ make_metadata.py /usr/local/etc/moonshot/pysaml_config.py > sp.xml
+This file you have to give to the person/organization that runs the AA you
+want to get information from.
+
+!!! That should be it !!!
+
diff --git a/freeradius_ecp.py b/freeradius_ecp.py
new file mode 100644 (file)
index 0000000..a1ac9d8
--- /dev/null
@@ -0,0 +1,260 @@
+#! /usr/bin/env python
+#
+# Copyright 2011 Roland Hedberg <roland.hedberg@adm.umu.se>
+#
+# The freeradius extension using ECP
+#
+__author__ = 'rolandh'
+
+import radiusd
+import saml2
+import sys
+import traceback
+
+from saml2 import saml
+
+from saml2.client import Saml2Client
+from saml2.s_utils import sid
+from saml2.response import authn_response
+from saml2.ecp_client import Client
+
+# Where's the configuration file is
+#CONFIG_DIR = "/usr/local/etc/moonshot"
+CONFIG_DIR = "../etc"
+sys.path.insert(0, CONFIG_DIR)
+
+import ecp_config
+
+# Globals
+CLIENT = None
+ECP = 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 exception_trace(tag, exc, log):
+    message = traceback.format_exception(*sys.exc_info())
+    log.error("[%s] ExcList: %s" % (tag, "".join(message),))
+    log.error("[%s] Exception: %s" % (tag, exc))
+
+
+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.
+    """
+    global CLIENT
+    global ECP
+
+    # Use IdP info retrieved from the SP when metadata is missing
+
+    try:
+        CLIENT = Saml2Client(ecp_config.DEBUG, config_file=ecp_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:
+        ECP = Client("", "", None, metadata_file=ecp_config.METADATA_FILE)
+    except Exception, err:
+        log(radiusd.L_ERR, str(err))
+        return -1
+
+    log(radiusd.L_INFO, 'ECP client initialized')
+
+    return 0
+
+
+def authentication_request(cls, ecp, idp_entity_id, destination,
+                           log=None, sign=False):
+    """ Does a authentication request to an Identity provider.
+    This function uses the SOAP binding other bindings could be used but are
+    not
+    supported right now.
+
+    :param cls: The SAML2 client instance
+    :param ecp: The ECP client instance
+    :param idp_entity_id: The identifier of the subject
+    :param destination: To whom the query should be sent
+    :param log: Function to use for logging
+    :param sign: Whether the request should be signed or not
+    :return: A Authentication Response
+    """
+
+    if log is None:
+        log = cls.logger
+
+    session_id = sid()
+    acsu = cls.config.endpoint('assertion_consumer_service',
+                               saml2.BINDING_PAOS)[0]
+    spentityid = cls.config.entityid
+
+    # create the request
+    request = cls.authn_request(session_id,
+                                destination,
+                                acsu,
+                                spentityid,
+                                "",
+                                log=LOG(),
+                                sign=sign,
+                                binding=saml2.BINDING_PAOS,
+                                nameid_format=saml.NAMEID_FORMAT_PERSISTENT)
+
+    try:
+        # send the request and receive the response
+        response = ecp.phase2(request, acsu, idp_entity_id)
+    except Exception, exc:
+        exception_trace("soap", exc, log)
+        if log:
+            log.info("SoapClient exception: %s" % (exc,))
+        return None
+
+    if response:
+        try:
+            # synchronous operation
+            aresp = authn_response(cls.config, acsu, log=log, asynchop=False,
+                                   allow_unsolicited=True)
+            #aresp.debug = True
+        except Exception, exc:
+            if log:
+                log.error("%s", (exc,))
+            return None
+
+        try:
+            _resp = aresp.load_instance(response).verify()
+        except Exception, err:
+            if log:
+                log.error("%s" % err)
+            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
+    attribute_service = CLIENT.config.attribute_services(ecp_config.IDP_ENTITYID)
+    location = attribute_service[0].location
+
+    log(radiusd.L_DBG, "location: %s" % location)
+
+
+    _assertion = authentication_request(CLIENT, ECP,
+                                        ecp_config.IDP_ENTITYID,
+                                        location,
+                                        log=LOG(),
+                                        sign=ecp_config.SIGN)
+
+    if _assertion is None:
+        return radiusd.RLM_MODULE_FAIL
+
+    if _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
+    _assertion.subject.subject_confirmation = []
+    # Only allow attributes that the service should have
+    try:
+        _assertion = only_allowed_attributes(CLIENT, _assertion,
+                                             ecp_config.ATTRIBUTE_FILTER[_srv])
+    except KeyError:
+        pass
+
+    log(radiusd.L_DBG, "Assertion: %s" % _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" % _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"')))
+  
\ No newline at end of file
diff --git a/freeradius_pysaml2.py b/freeradius_pysaml2.py
new file mode 100755 (executable)
index 0000000..2dae767
--- /dev/null
@@ -0,0 +1,269 @@
+#! /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"')))
diff --git a/radiusd.py b/radiusd.py
new file mode 100644 (file)
index 0000000..2bda4db
--- /dev/null
@@ -0,0 +1,41 @@
+#! /usr/bin/env python
+#
+# Definitions for RADIUS programs
+#
+# Copyright 2002 Miguel A.L. Paraz <mparaz@mparaz.com>
+#
+# This should only be used when testing modules.
+# Inside freeradius, the 'radiusd' Python module is created by the C module
+# and the definitions are automatically created.
+#
+# $Id$
+
+# from modules.h
+
+RLM_MODULE_REJECT = 0
+RLM_MODULE_FAIL = 1
+RLM_MODULE_OK = 2
+RLM_MODULE_HANDLED = 3
+RLM_MODULE_INVALID = 4
+RLM_MODULE_USERLOCK = 5
+RLM_MODULE_NOTFOUND = 6
+RLM_MODULE_NOOP = 7
+RLM_MODULE_UPDATED = 8
+RLM_MODULE_NUMCODES = 9
+
+
+# from radiusd.h
+L_DBG = 1
+L_AUTH = 2
+L_INFO = 3
+L_ERR = 4
+L_PROXY = 5
+L_CONS = 128
+
+
+# log function
+def radlog(level, msg):
+    import sys
+    sys.stdout.write("<python>" + msg + '\n')
+    level = level
+  
\ No newline at end of file
diff --git a/setup.py b/setup.py
new file mode 100644 (file)
index 0000000..c75840b
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env python
+
+import os
+
+"""Setup script for the pyparsing module distribution."""
+from distutils.core import setup
+
+__author__ = 'rolandh'
+
+def read(fname):
+    return open(os.path.join(os.path.dirname(__file__), fname)).read()
+
+setup(# Distribution meta-data
+    name = "freeradius_pysaml2",
+    version = "0.0.4",
+    description = "FreeRadius python module to be used in Moonshot",
+    author = "Roland Hedberg",
+    author_email = "roland.hedberg@adm.umu.se",
+    license = "MIT License",
+    py_modules = ['freeradius_pysaml2','radiusd', "freeradius_ecp"],
+    classifiers=[
+        'Development Status :: 4 - Beta',
+        'Intended Audience :: Developers',
+        'Intended Audience :: Information Technology',
+        'License :: OSI Approved :: MIT License',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        ],
+    long_description=read('README'),
+    data_files=[('/usr/local/etc/moonshot', ['etc/config.py',
+                                             'etc/metadata.xml',
+                                             "etc/pysaml_config.py"]),
+                ('/usr/local/etc/moonshot/attributemaps',
+                            ['attributemaps/basic.py',
+                             'attributemaps/saml_uri.py',
+                             'attributemaps/shibboleth_uri.py']),
+                ('/usr/local/etc/moonshot/pki',
+                            ['pki/ssl.cert', 'pki/ssl.key']),
+                ('/usr/local/etc/moonshot/template',
+                            ['template/modules_python',
+                             'template/sites-available_default',
+                             'template/sites-available_inner-tunnel'])],
+    install_requires=[
+        'pysaml2'
+    ]
+    )