Initial commits
[freeradius-pysaml2.git] / freeradius_ecp.py
1 #! /usr/bin/env python
2 #
3 # Copyright 2011 Roland Hedberg <roland.hedberg@adm.umu.se>
4 #
5 # The freeradius extension using ECP
6 #
7 __author__ = 'rolandh'
8
9 import radiusd
10 import saml2
11 import sys
12 import traceback
13
14 from saml2 import saml
15
16 from saml2.client import Saml2Client
17 from saml2.s_utils import sid
18 from saml2.response import authn_response
19 from saml2.ecp_client import Client
20
21 # Where's the configuration file is
22 #CONFIG_DIR = "/usr/local/etc/moonshot"
23 CONFIG_DIR = "../etc"
24 sys.path.insert(0, CONFIG_DIR)
25
26 import ecp_config
27
28 # Globals
29 CLIENT = None
30 ECP = None
31
32 def eq_len_parts(str, delta=250):
33     res = []
34     n = 0
35     strlen = len(str)
36     while n <= strlen:
37         m = n + delta
38         res.append("".join(str[n:m]))
39         n = m
40     return res
41
42 def exception_trace(tag, exc, log):
43     message = traceback.format_exception(*sys.exc_info())
44     log.error("[%s] ExcList: %s" % (tag, "".join(message),))
45     log.error("[%s] Exception: %s" % (tag, exc))
46
47
48 def log(level, s):
49     """Log function."""
50     radiusd.radlog(level, 'moonshot.py: ' + s)
51
52
53 class LOG(object):
54     def info(self, txt):
55         log(radiusd.L_INFO, txt)
56
57     def error(self, txt):
58         log(radiusd.L_ERR, txt)
59
60
61 #noinspection PyUnusedLocal
62 def instantiate(p):
63     """Module Instantiation.  0 for success, -1 for failure.
64     """
65     global CLIENT
66     global ECP
67
68     # Use IdP info retrieved from the SP when metadata is missing
69
70     try:
71         CLIENT = Saml2Client(ecp_config.DEBUG, config_file=ecp_config.CONFIG)
72
73     except Exception, e:
74         # Report the error and return -1 for failure.
75         # xxx A more advanced module would retry the database.
76         log(radiusd.L_ERR, str(e))
77         return -1
78
79     try:
80         ECP = Client("", "", None, metadata_file=ecp_config.METADATA_FILE)
81     except Exception, err:
82         log(radiusd.L_ERR, str(err))
83         return -1
84
85     log(radiusd.L_INFO, 'ECP client initialized')
86
87     return 0
88
89
90 def authentication_request(cls, ecp, idp_entity_id, destination,
91                            log=None, sign=False):
92     """ Does a authentication request to an Identity provider.
93     This function uses the SOAP binding other bindings could be used but are
94     not
95     supported right now.
96
97     :param cls: The SAML2 client instance
98     :param ecp: The ECP client instance
99     :param idp_entity_id: The identifier of the subject
100     :param destination: To whom the query should be sent
101     :param log: Function to use for logging
102     :param sign: Whether the request should be signed or not
103     :return: A Authentication Response
104     """
105
106     if log is None:
107         log = cls.logger
108
109     session_id = sid()
110     acsu = cls.config.endpoint('assertion_consumer_service',
111                                saml2.BINDING_PAOS)[0]
112     spentityid = cls.config.entityid
113
114     # create the request
115     request = cls.authn_request(session_id,
116                                 destination,
117                                 acsu,
118                                 spentityid,
119                                 "",
120                                 log=LOG(),
121                                 sign=sign,
122                                 binding=saml2.BINDING_PAOS,
123                                 nameid_format=saml.NAMEID_FORMAT_PERSISTENT)
124
125     try:
126         # send the request and receive the response
127         response = ecp.phase2(request, acsu, idp_entity_id)
128     except Exception, exc:
129         exception_trace("soap", exc, log)
130         if log:
131             log.info("SoapClient exception: %s" % (exc,))
132         return None
133
134     if response:
135         try:
136             # synchronous operation
137             aresp = authn_response(cls.config, acsu, log=log, asynchop=False,
138                                    allow_unsolicited=True)
139             #aresp.debug = True
140         except Exception, exc:
141             if log:
142                 log.error("%s", (exc,))
143             return None
144
145         try:
146             _resp = aresp.load_instance(response).verify()
147         except Exception, err:
148             if log:
149                 log.error("%s" % err)
150             return None
151         if _resp is None:
152             if log:
153                 log.error("Didn't like the response")
154             return None
155
156         return _resp.assertion
157     else:
158         return None
159
160
161 def only_allowed_attributes(client, assertion, allowed):
162     res = []
163     _aconvs = client.config.attribute_converters
164
165     for statement in assertion.attribute_statement:
166         for attribute in statement.attribute:
167             if attribute.friendly_name:
168                 fname = attribute.friendly_name
169             else:
170                 fname = ""
171                 for acv in _aconvs:
172                     if acv.name_form == attribute.name_form:
173                         fname = acv._fro[attribute.name]
174
175             if fname in allowed:
176                 res.append(attribute)
177
178     return assertion
179
180
181 def post_auth(authData):
182     """ Attribute aggregation after authentication
183     This is the function that is accessible from the freeradius server core.
184
185     :return: A 3-tuple
186     """
187
188     global CLIENT
189     global HTTP
190
191     # Extract the data we need.
192     userName = None
193     serviceName = ""
194     hostName = ""
195     #userPasswd = None
196
197     for t in authData:
198         if t[0] == 'User-Name':
199             userName = t[1][1:-1]
200         elif t[0] == "GSS-Acceptor-Service-Name":
201             serviceName = t[1][1:-1]
202         elif t[0] == "GSS-Acceptor-Host-Name":
203             hostName = t[1][1:-1]
204
205     _srv = "%s:%s" % (serviceName, hostName)
206     log(radiusd.L_DBG, "Working on behalf of: %s" % _srv)
207
208
209     # Find the endpoint to use
210     attribute_service = CLIENT.config.attribute_services(ecp_config.IDP_ENTITYID)
211     location = attribute_service[0].location
212
213     log(radiusd.L_DBG, "location: %s" % location)
214
215
216     _assertion = authentication_request(CLIENT, ECP,
217                                         ecp_config.IDP_ENTITYID,
218                                         location,
219                                         log=LOG(),
220                                         sign=ecp_config.SIGN)
221
222     if _assertion is None:
223         return radiusd.RLM_MODULE_FAIL
224
225     if _assertion is False:
226         log(radiusd.L_DBG, "IdP returned: %s" % HTTP.server.error_description)
227         return radiusd.RLM_MODULE_FAIL
228
229     # remove the subject confirmation if there is one
230     _assertion.subject.subject_confirmation = []
231     # Only allow attributes that the service should have
232     try:
233         _assertion = only_allowed_attributes(CLIENT, _assertion,
234                                              ecp_config.ATTRIBUTE_FILTER[_srv])
235     except KeyError:
236         pass
237
238     log(radiusd.L_DBG, "Assertion: %s" % _assertion)
239
240     # Log the success
241     log(radiusd.L_DBG, 'user accepted: %s' % (userName, ))
242
243     # We are adding to the RADIUS packet
244     # We need to set an Auth-Type.
245
246     # UKERNA, 25622; attribute ID is 132
247     attr = "SAML-AAA-Assertion"
248     #attr = "UKERNA-Attr-%d" % 132
249     #attr = "Vendor-%d-Attr-%d" % (25622, 132)
250     restup = (tuple([(attr, x) for x in eq_len_parts("%s" % _assertion, 248)]))
251
252     return radiusd.RLM_MODULE_UPDATED, restup, None
253
254
255 # Test the modules
256 if __name__ == '__main__':
257     instantiate(None)
258     #    print authorize((('User-Name', '"map"'), ('User-Password', '"abc"')))
259     print post_auth((('User-Name', '"roland"'), ('User-Password', '"one"')))
260