|
52 |
import threading |
52 |
import threading |
53 |
from errno import ESRCH |
53 |
from errno import ESRCH |
54 |
from optparse import OptionParser |
54 |
from optparse import OptionParser |
55 |
from six.moves.urllib_parse import urlparse, urlunsplit |
55 |
from six.moves.urllib_parse import urlparse, urlunsplit, urlencode |
56 |
from six.moves.http_client import REQUEST_ENTITY_TOO_LARGE, LENGTH_REQUIRED, NOT_FOUND, BAD_REQUEST, UNAUTHORIZED, SERVICE_UNAVAILABLE |
56 |
from six.moves.http_client import REQUEST_ENTITY_TOO_LARGE, LENGTH_REQUIRED, NOT_FOUND, BAD_REQUEST, UNAUTHORIZED, SERVICE_UNAVAILABLE |
57 |
|
57 |
|
58 |
import six |
58 |
import six |
|
388 |
except ValueError: # no SAML, no client |
388 |
except ValueError: # no SAML, no client |
389 |
return 0 |
389 |
return 0 |
390 |
|
390 |
|
|
|
391 |
@property |
392 |
def is_external_user(self): |
393 |
return self.is_saml_user() and self.saml.issuer != ucr.get('umc/saml/idp-server') |
394 |
|
391 |
def is_saml_user(self): |
395 |
def is_saml_user(self): |
392 |
# self.saml indicates that it was originally a |
396 |
# self.saml indicates that it was originally a |
393 |
# saml user. but it may have upgraded and got a |
397 |
# saml user. but it may have upgraded and got a |
|
416 |
|
420 |
|
417 |
class SAMLUser(object): |
421 |
class SAMLUser(object): |
418 |
|
422 |
|
419 |
__slots__ = ('message', 'username', 'not_on_or_after', 'name_id') |
423 |
__slots__ = ('message', 'username', 'not_on_or_after', 'name_id', 'issuer', 'attributes') |
420 |
|
424 |
|
421 |
def __init__(self, response, message): |
425 |
def __init__(self, response, message): |
422 |
self.not_on_or_after = response.not_on_or_after |
426 |
self.not_on_or_after = response.not_on_or_after |
423 |
self.name_id = code(response.name_id) |
427 |
self.name_id = code(response.name_id) |
424 |
self.message = message |
428 |
self.message = message |
425 |
self.username = u''.join(response.ava['uid']) |
429 |
self.username = u''.join(response.ava['uid']) |
|
|
430 |
self.attributes = response.ava.copy() |
431 |
self.issuer = response.issuer() |
426 |
|
432 |
|
427 |
@property |
433 |
@property |
428 |
def time_remaining(self): |
434 |
def time_remaining(self): |
Lines 597-605
class SamlError(UMC_HTTPError):
|
Link Here
|
---|
|
597 |
def no_identity_provider(self): |
603 |
def no_identity_provider(self): |
598 |
return self._('There is a configuration error in the service provider: No identity provider are set up for use.') |
604 |
return self._('There is a configuration error in the service provider: No identity provider are set up for use.') |
599 |
|
605 |
|
600 |
@error # TODO: multiple choices redirection status |
606 |
@error(status=400) # 300 is more correct, but firefox uses the first link and redirects |
601 |
def multiple_identity_provider(self, idps, idp_query_param): |
607 |
def multiple_identity_provider(self, idps, idp_query_param, base_uri): |
602 |
return self._('Could not pick an identity provider. You can specify one via the query string parameter %(param)r from %(idps)r') % {'param': idp_query_param, 'idps': idps} |
608 |
uris = [base_uri + ('&', '?')[urlparse(base_uri).query == ''] + urlencode({ |
|
|
609 |
idp_query_param: idp |
610 |
}) for idp in idps] |
611 |
cherrypy.response.headers['Location'] = ', '.join(uris) |
612 |
raise HTTPRedirect(uris, 300) # FIXME |
613 |
return self._('Could not pick an identity provider. Choose from: %s') % ('\n'.join(uris),) |
603 |
|
614 |
|
604 |
|
615 |
|
605 |
class Ressource(object): |
616 |
class Ressource(object): |
Lines 806-812
class CPgeneric(Ressource):
|
Link Here
|
---|
|
806 |
|
817 |
|
807 |
user = self.get_user() |
818 |
user = self.get_user() |
808 |
client = UMCP_Dispatcher.sessions.get(sessionid) |
819 |
client = UMCP_Dispatcher.sessions.get(sessionid) |
809 |
if user and (user.password or user.saml) and (not client or not client.authenticated): |
820 |
if user and not user.is_external_user and (user.password or user.saml) and (not client or not client.authenticated): |
810 |
auth = Request('AUTH') |
821 |
auth = Request('AUTH') |
811 |
auth.body = { |
822 |
auth.body = { |
812 |
'username': user.username, |
823 |
'username': user.username, |
Lines 894-899
class CPGet(CPgeneric):
|
Link Here
|
---|
|
894 |
raise UMC_HTTPError(UNAUTHORIZED) |
905 |
raise UMC_HTTPError(UNAUTHORIZED) |
895 |
info['username'] = user.username |
906 |
info['username'] = user.username |
896 |
info['auth_type'] = user.saml and 'SAML' |
907 |
info['auth_type'] = user.saml and 'SAML' |
|
|
908 |
info['attributes'] = user.saml and user.saml.attributes # TODO: create a own URL for this? |
897 |
info['remaining'] = user.time_remaining |
909 |
info['remaining'] = user.time_remaining |
898 |
info['validity'] = user.session_validity |
910 |
info['validity'] = user.session_validity |
899 |
return json.dumps({"status": 200, "result": info, "message": ""}).encode('ASCII') |
911 |
return json.dumps({"status": 200, "result": info, "message": ""}).encode('ASCII') |
Lines 1173-1179
class SAML(Ressource):
|
Link Here
|
---|
|
1173 |
self.configfile = '/usr/share/univention-management-console/saml/sp.py' |
1185 |
self.configfile = '/usr/share/univention-management-console/saml/sp.py' |
1174 |
self.__sp = None |
1186 |
self.__sp = None |
1175 |
|
1187 |
|
1176 |
self.idp_query_param = "IdpQuery" |
1188 |
self.idp_query_param = "idp" |
1177 |
self.bindings = [BINDING_HTTP_REDIRECT, BINDING_HTTP_POST, BINDING_HTTP_ARTIFACT] |
1189 |
self.bindings = [BINDING_HTTP_REDIRECT, BINDING_HTTP_POST, BINDING_HTTP_ARTIFACT] |
1178 |
|
1190 |
|
1179 |
self.outstanding_queries = {} |
1191 |
self.outstanding_queries = {} |
Lines 1206-1212
class SAML(Ressource):
|
Link Here
|
---|
|
1206 |
binding, message, relay_state = self._get_saml_message() |
1218 |
binding, message, relay_state = self._get_saml_message() |
1207 |
|
1219 |
|
1208 |
if message is None: |
1220 |
if message is None: |
1209 |
return self.do_single_sign_on(relay_state=kwargs.get('location', '/univention/management/')) |
1221 |
return self.do_single_sign_on(relay_state=kwargs.get('location', '/univention/management/'), use_ucs_identity_provider='external' not in kwargs) |
1210 |
|
1222 |
|
1211 |
acs = self.attribute_consuming_service |
1223 |
acs = self.attribute_consuming_service |
1212 |
if relay_state == 'iframe-passive': |
1224 |
if relay_state == 'iframe-passive': |
Lines 1216-1222
class SAML(Ressource):
|
Link Here
|
---|
|
1216 |
@cherrypy.expose |
1228 |
@cherrypy.expose |
1217 |
def iframe(self, *args, **kwargs): |
1229 |
def iframe(self, *args, **kwargs): |
1218 |
cherrypy.request.uri = cherrypy.request.uri.replace('/iframe', '') |
1230 |
cherrypy.request.uri = cherrypy.request.uri.replace('/iframe', '') |
1219 |
return self.do_single_sign_on(is_passive='true', relay_state='iframe-passive') |
1231 |
return self.do_single_sign_on(is_passive='true', relay_state='iframe-passive', use_ucs_identity_provider=True) |
1220 |
|
1232 |
|
1221 |
def attribute_consuming_service(self, binding, message, relay_state): |
1233 |
def attribute_consuming_service(self, binding, message, relay_state): |
1222 |
response = self.acs(message, binding) |
1234 |
response = self.acs(message, binding) |
Lines 1355-1361
class SAML(Ressource):
|
Link Here
|
---|
|
1355 |
|
1367 |
|
1356 |
Returns (binding, http-arguments) |
1368 |
Returns (binding, http-arguments) |
1357 |
""" |
1369 |
""" |
1358 |
identity_provider_entity_id = self.select_identity_provider() |
1370 |
identity_provider_entity_id = self.select_identity_provider(use_ucs_identity_provider=kwargs.pop('use_ucs_identity_provider', True)) |
1359 |
binding, destination = self.get_identity_provider_destination(identity_provider_entity_id) |
1371 |
binding, destination = self.get_identity_provider_destination(identity_provider_entity_id) |
1360 |
|
1372 |
|
1361 |
relay_state = kwargs.pop('relay_state', None) |
1373 |
relay_state = kwargs.pop('relay_state', None) |
Lines 1367-1373
class SAML(Ressource):
|
Link Here
|
---|
|
1367 |
self.outstanding_queries[sid] = service_provider_url # cherrypy.request.uri # TODO: shouldn't this contain service_provider_url? |
1379 |
self.outstanding_queries[sid] = service_provider_url # cherrypy.request.uri # TODO: shouldn't this contain service_provider_url? |
1368 |
return binding, http_args |
1380 |
return binding, http_args |
1369 |
|
1381 |
|
1370 |
def select_identity_provider(self): |
1382 |
def select_identity_provider(self, use_ucs_identity_provider=True): |
1371 |
"""Select an identity provider based on the available identity providers. |
1383 |
"""Select an identity provider based on the available identity providers. |
1372 |
If multiple IDP's are set up the client might have specified one in the query string. |
1384 |
If multiple IDP's are set up the client might have specified one in the query string. |
1373 |
Otherwise an error is raised where the user can choose one. |
1385 |
Otherwise an error is raised where the user can choose one. |
Lines 1383-1389
class SAML(Ressource):
|
Link Here
|
---|
|
1383 |
return list(idps.keys())[0] |
1395 |
return list(idps.keys())[0] |
1384 |
if not idps: |
1396 |
if not idps: |
1385 |
raise SamlError().no_identity_provider() |
1397 |
raise SamlError().no_identity_provider() |
1386 |
raise SamlError().multiple_identity_provider(list(idps.keys()), self.idp_query_param) |
1398 |
if use_ucs_identity_provider: |
|
|
1399 |
for key in idps: |
1400 |
if key == ucr.get('umc/saml/idp-server'): |
1401 |
return key |
1402 |
raise SamlError().multiple_identity_provider(list(idps.keys()), self.idp_query_param, cherrypy.request.uri) |
1387 |
|
1403 |
|
1388 |
def get_identity_provider_destination(self, entity_id): |
1404 |
def get_identity_provider_destination(self, entity_id): |
1389 |
"""Get the destination (with SAML binding) of the specified entity_id. |
1405 |
"""Get the destination (with SAML binding) of the specified entity_id. |