Added description of files that should be part of the package
[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         ECP = Client("", config.PASSWD, None,
82                      metadata_file=config.METADATA_FILE)
83     except Exception, err:
84         log(radiusd.L_ERR, str(err))
85         return -1
86
87     log(radiusd.L_INFO, 'ECP client initialized')
88
89     return 0
90
91
92 def authentication_request(cls, ecp, idp_entity_id, destination,
93                            log=None, sign=False):
94     """ Does a authentication request to an Identity provider.
95     This function uses the SOAP binding other bindings could be used but are
96     not
97     supported right now.
98
99     :param cls: The SAML2 client instance
100     :param ecp: The ECP client instance
101     :param idp_entity_id: The identifier of the subject
102     :param destination: To whom the query should be sent
103     :param log: Function to use for logging
104     :param sign: Whether the request should be signed or not
105     :return: A Authentication Response
106     """
107
108     if log is None:
109         log = cls.logger
110
111     session_id = sid()
112     acsus = cls.config.endpoint('assertion_consumer_service',
113                                 saml2.BINDING_PAOS)
114     if not acsus:
115         if log:
116             log.error("Couldn't find own PAOS endpoint for")
117     acsu = acsus[0]
118
119     spentityid = cls.config.entityid
120
121     # create the request
122     request = cls.authn_request(session_id,
123                                 destination,
124                                 acsu,
125                                 spentityid,
126                                 "",
127                                 log=LOG(),
128                                 sign=sign,
129                                 binding=saml2.BINDING_PAOS,
130                                 nameid_format=saml.NAMEID_FORMAT_PERSISTENT)
131
132     try:
133         # send the request and receive the response
134         response = ecp.phase2(request, acsu, idp_entity_id)
135     except Exception, exc:
136         exception_trace("soap", exc, log)
137         if log:
138             log.info("SoapClient exception: %s" % (exc,))
139         return None
140
141     if response:
142         try:
143             # synchronous operation
144             aresp = authn_response(cls.config, acsu, log=log, asynchop=False,
145                                    allow_unsolicited=True)
146             #aresp.debug = True
147         except Exception, exc:
148             if log:
149                 log.error("%s", (exc,))
150             return None
151
152         try:
153             _resp = aresp.load_instance(response).verify()
154         except Exception, err:
155             if log:
156                 log.error("%s" % err)
157             return None
158         if _resp is None:
159             if log:
160                 log.error("Didn't like the response")
161             return None
162
163         return _resp.assertion
164     else:
165         return None
166
167
168 def only_allowed_attributes(client, assertion, allowed):
169     res = []
170     _aconvs = client.config.attribute_converters
171
172     for statement in assertion.attribute_statement:
173         for attribute in statement.attribute:
174             if attribute.friendly_name:
175                 fname = attribute.friendly_name
176             else:
177                 fname = ""
178                 for acv in _aconvs:
179                     if acv.name_form == attribute.name_form:
180                         fname = acv._fro[attribute.name]
181
182             if fname in allowed:
183                 res.append(attribute)
184
185     return assertion
186
187
188 def post_auth(authData):
189     """ Attribute aggregation after authentication
190     This is the function that is accessible from the freeradius server core.
191
192     :return: A 3-tuple
193     """
194
195     global CLIENT
196     global HTTP
197     global ECP
198
199     # Extract the data we need.
200     userName = None
201     serviceName = ""
202     hostName = ""
203
204     for t in authData:
205         if t[0] == 'User-Name':
206             userName = t[1][1:-1]
207         elif t[0] == "GSS-Acceptor-Service-Name":
208             serviceName = t[1][1:-1]
209         elif t[0] == "GSS-Acceptor-Host-Name":
210             hostName = t[1][1:-1]
211
212     _srv = "%s:%s" % (serviceName, hostName)
213     log(radiusd.L_DBG, "Working on behalf of: %s" % _srv)
214
215
216     # Find the endpoint to use
217     sso_service = CLIENT.config.single_sign_on_services(config.IDP_ENTITYID,
218                                                         saml2.BINDING_PAOS)
219     if not sso_service:
220         log(radiusd.L_DBG,
221             "Couldn't find an single sign on endpoint for: %s" % (
222                 config.IDP_ENTITYID,))
223         return radiusd.RLM_MODULE_FAIL
224
225     location = sso_service[0]
226
227     log(radiusd.L_DBG, "location: %s" % location)
228
229     ECP.http.clear_credentials()
230     ECP.user = userName
231     log(radiusd.L_DBG, "Login using user:%s password:'%s'" % (ECP.user,
232                                                              ECP.passwd))
233
234     _assertion = authentication_request(CLIENT, ECP,
235                                         config.IDP_ENTITYID,
236                                         location,
237                                         log=LOG(),
238                                         sign=config.SIGN)
239
240     if _assertion is None:
241         return radiusd.RLM_MODULE_FAIL
242
243     if _assertion is False:
244         log(radiusd.L_DBG, "IdP returned: %s" % HTTP.server.error_description)
245         return radiusd.RLM_MODULE_FAIL
246
247     # remove the subject confirmation if there is one
248     _assertion.subject.subject_confirmation = []
249
250     log(radiusd.L_DBG, "Assertion: %s" % _assertion)
251
252     # Log the success
253     log(radiusd.L_DBG, 'user accepted: %s' % (userName, ))
254
255     # We are adding to the RADIUS packet
256     # We need to set an Auth-Type.
257
258     # UKERNA, 25622; attribute ID is 132
259     attr = "SAML-AAA-Assertion"
260     #attr = "UKERNA-Attr-%d" % 132
261     #attr = "Vendor-%d-Attr-%d" % (25622, 132)
262     restup = (tuple([(attr, x) for x in eq_len_parts("%s" % _assertion, 248)]))
263
264     return radiusd.RLM_MODULE_UPDATED, restup, None
265
266
267 # Test the modules
268 if __name__ == '__main__':
269     instantiate(None)
270     #    print authorize((('User-Name', '"map"'), ('User-Password', '"abc"')))
271     print post_auth((('User-Name', '"roland"'), ('User-Password', '"one"')))
272