3 # Copyright 2011 Roland Hedberg <roland.hedberg@adm.umu.se>
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
16 # Where's the configuration
17 CONFIG_DIR = "/usr/local/etc/moonshot"
18 sys.path.insert(0, CONFIG_DIR)
27 def eq_len_parts(str, delta=250):
33 res.append("".join(str[n:m]))
40 radiusd.radlog(level, 'moonshot.py: ' + s)
45 log(radiusd.L_INFO, txt)
48 log(radiusd.L_ERR, txt)
50 #noinspection PyUnusedLocal
52 """Module Instantiation. 0 for success, -1 for failure.
53 p is a dummy variable here.
59 CLIENT = Saml2Client(config.DEBUG,
60 identity_cache=config.IDENTITY_CACHE,
61 state_cache=config.STATE_CACHE,
62 config_file=config.CONFIG)
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))
71 HTTP = soap.SOAPClient("") # No default URL
73 log(radiusd.L_ERR, str(e))
76 log(radiusd.L_INFO, 'SP initialized')
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
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
107 issuer = cls.issuer(issuer_id)
109 if not name_qualifier and not sp_name_qualifier:
110 sp_name_qualifier = cls.config.entityid
112 request = cls.create_attribute_query(session_id, subject_id,
113 destination, issuer, attribute,
116 nameid_format=nameid_format, sign=sign)
118 # soapclient = HTTP.send(destination, cls.config.key_file,
119 # cls.config.cert_file)
122 response = HTTP.send(request, path=destination)
123 except Exception, exc:
125 log.info("SoapClient exception: %s" % (exc,))
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
136 except Exception, exc:
138 log.error("%s", (exc,))
142 _resp = aresp.loads(response, False, HTTP.response).verify()
143 except Exception, err:
145 log.error("%s", (exc,))
149 log.error("Didn't like the response")
152 return _resp.assertion
157 def only_allowed_attributes(client, assertion, allowed):
159 _aconvs = client.config.attribute_converters
161 for statement in assertion.attribute_statement:
162 for attribute in statement.attribute:
163 if attribute.friendly_name:
164 fname = attribute.friendly_name
168 if acv.name_form == attribute.name_form:
169 fname = acv._fro[attribute.name]
172 res.append(attribute)
177 def post_auth(authData):
178 """ Attribute aggregation after authentication
179 This is the function that is accessible from the freeradius server core.
187 # Extract the data we need.
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]
201 _srv = "%s:%s" % (serviceName, hostName)
202 log(radiusd.L_DBG, "Working on behalf of: %s" % _srv)
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)
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
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)
219 _attribute_assertion = attribute_query(CLIENT,
222 sp_name_qualifier=sp_name_qualifier,
223 name_qualifier=name_qualifier,
224 nameid_format=nameid_format,
225 issuer_id=CLIENT.issuer(),
229 if _attribute_assertion is None:
230 return radiusd.RLM_MODULE_FAIL
232 if _attribute_assertion is False:
233 log(radiusd.L_DBG, "IdP returned: %s" % HTTP.server.error_description)
234 return radiusd.RLM_MODULE_FAIL
236 # remove the subject confirmation if there is one
237 _attribute_assertion.subject.subject_confirmation = []
238 # Only allow attributes that the service should have
240 _attribute_assertion = only_allowed_attributes(CLIENT,
241 _attribute_assertion,
242 config.ATTRIBUTE_FILTER[
247 log(radiusd.L_DBG, "Assertion: %s" % _attribute_assertion)
250 log(radiusd.L_DBG, 'user accepted: %s' % (userName, ))
252 # We are adding to the RADIUS packet
253 # We need to set an Auth-Type.
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)]))
262 return radiusd.RLM_MODULE_UPDATED, restup, None
266 if __name__ == '__main__':
268 # print authorize((('User-Name', '"map"'), ('User-Password', '"abc"')))
269 print post_auth((('User-Name', '"roland"'), ('User-Password', '"one"')))