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