From 274fce3387186b9b0988451bdbb3ccf628155cc1 Mon Sep 17 00:00:00 2001 From: rohe0002 Date: Mon, 24 Oct 2011 15:05:53 +0200 Subject: [PATCH] Initial commit --- README | 78 +++++++++++++++ freeradius_ecp.py | 260 ++++++++++++++++++++++++++++++++++++++++++++++++ freeradius_pysaml2.py | 269 ++++++++++++++++++++++++++++++++++++++++++++++++++ radiusd.py | 41 ++++++++ setup.py | 46 +++++++++ 5 files changed, 694 insertions(+) create mode 100644 README create mode 100644 freeradius_ecp.py create mode 100755 freeradius_pysaml2.py create mode 100644 radiusd.py create mode 100644 setup.py diff --git a/README b/README new file mode 100644 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 index 0000000..a1ac9d8 --- /dev/null +++ b/freeradius_ecp.py @@ -0,0 +1,260 @@ +#! /usr/bin/env python +# +# Copyright 2011 Roland Hedberg +# +# 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 index 0000000..2dae767 --- /dev/null +++ b/freeradius_pysaml2.py @@ -0,0 +1,269 @@ +#! /usr/bin/env python +# +# Copyright 2011 Roland Hedberg +# +# $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 index 0000000..2bda4db --- /dev/null +++ b/radiusd.py @@ -0,0 +1,41 @@ +#! /usr/bin/env python +# +# Definitions for RADIUS programs +# +# Copyright 2002 Miguel A.L. Paraz +# +# 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("" + msg + '\n') + level = level + \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 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' + ] + ) -- 2.1.4