Support for HTTPS certificate verification or not.
[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: {0:>s}".format(headers)
144             
145         # send the request and receive the response
146         response = ecp.phase2(request, acsu, idp_entity_id, headers,
147                               destination)
148     except Exception, exc:
149         exception_trace("soap", exc, log)
150         if log:
151             log.info("SoapClient exception: %s" % (exc,))
152         return None
153
154     if response:
155         try:
156             # synchronous operation
157             aresp = authn_response(cls.config, acsu, log=log, asynchop=False,
158                                    allow_unsolicited=True)
159             #aresp.debug = True
160         except Exception, exc:
161             if log:
162                 log.error("%s", (exc,))
163             return None
164
165         try:
166             _resp = aresp.load_instance(response).verify()
167         except Exception, err:
168             if log:
169                 log.error("%s" % err)
170             return None
171         if _resp is None:
172             if log:
173                 log.error("Didn't like the response")
174             return None
175
176         return _resp.assertion
177     else:
178         return None
179
180
181 def only_allowed_attributes(client, assertion, allowed):
182     res = []
183     _aconvs = client.config.attribute_converters
184
185     for statement in assertion.attribute_statement:
186         for attribute in statement.attribute:
187             if attribute.friendly_name:
188                 fname = attribute.friendly_name
189             else:
190                 fname = ""
191                 for acv in _aconvs:
192                     if acv.name_form == attribute.name_form:
193                         fname = acv._fro[attribute.name]
194
195             if fname in allowed:
196                 res.append(attribute)
197
198     return assertion
199
200
201 def post_auth(authData):
202     """ Attribute aggregation after authentication
203     This is the function that is accessible from the freeradius server core.
204
205     :return: A 3-tuple
206     """
207
208     global CLIENT
209     global HTTP
210     global ECP
211
212     # Extract the data we need.
213     userName = None
214     serviceName = ""
215     hostName = ""
216
217     for t in authData:
218         if t[0] == 'User-Name':
219             userName = t[1][1:-1]
220         elif t[0] == "GSS-Acceptor-Service-Name":
221             serviceName = t[1][1:-1]
222         elif t[0] == "GSS-Acceptor-Host-Name":
223             hostName = t[1][1:-1]
224
225     _srv = "%s:%s" % (serviceName, hostName)
226     log(radiusd.L_DBG, "Working on behalf of: %s" % _srv)
227
228     # Find the endpoint to use
229     sso_service = CLIENT.config.single_sign_on_services(config.IDP_ENTITYID,
230                                                         saml2.BINDING_PAOS)
231     if not sso_service:
232         log(radiusd.L_DBG,
233             "Couldn't find an single-sign-on endpoint for: %s" % (
234                 config.IDP_ENTITYID,))
235         return radiusd.RLM_MODULE_FAIL
236
237     location = sso_service[0]
238
239     log(radiusd.L_DBG, "location: %s" % location)
240
241     #ECP.http.clear_credentials()
242     ECP.user = userName
243     log(radiusd.L_DBG, "Login using user:%s password:'%s'" % (ECP.user,
244                                                              ECP.passwd))
245
246     _assertion = authentication_request(CLIENT, ECP,
247                                         config.IDP_ENTITYID,
248                                         location,
249                                         log=LOG(),
250                                         sign=config.SIGN)
251
252     if _assertion is None:
253         return radiusd.RLM_MODULE_FAIL
254
255     if _assertion is False:
256         log(radiusd.L_DBG, "IdP returned: %s" % HTTP.server.error_description)
257         return radiusd.RLM_MODULE_FAIL
258
259     # remove the subject confirmation if there is one
260     _assertion.subject.subject_confirmation = []
261
262     log(radiusd.L_DBG, "Assertion: %s" % _assertion)
263
264     # Log the success
265     log(radiusd.L_DBG, 'user accepted: %s' % (userName, ))
266
267     # We are adding to the RADIUS packet
268     # We need to set an Auth-Type.
269
270     # UKERNA, 25622; attribute ID is 132
271     attr = "SAML-AAA-Assertion"
272     #attr = "UKERNA-Attr-%d" % 132
273     #attr = "Vendor-%d-Attr-%d" % (25622, 132)
274     restup = (tuple([(attr, x) for x in eq_len_parts("%s" % _assertion, 247)]))
275
276     return radiusd.RLM_MODULE_UPDATED, restup, None
277
278
279 # Test the modules
280 if __name__ == '__main__':
281     instantiate(None)
282     #    print authorize((('User-Name', '"map"'), ('User-Password', '"abc"')))
283     print post_auth((('User-Name', '"roland"'), ('User-Password', '"one"')))
284