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