diff options
-rw-r--r-- | install/conf/ipa.conf | 8 | ||||
-rw-r--r-- | ipaserver/plugins/ldap2.py | 14 | ||||
-rw-r--r-- | ipaserver/plugins/xmlserver.py | 3 | ||||
-rw-r--r-- | ipaserver/rpcserver.py | 110 |
4 files changed, 126 insertions, 9 deletions
diff --git a/install/conf/ipa.conf b/install/conf/ipa.conf index f4dac9827..7eede73ef 100644 --- a/install/conf/ipa.conf +++ b/install/conf/ipa.conf @@ -1,5 +1,5 @@ # -# VERSION 15 - DO NOT REMOVE THIS LINE +# VERSION 16 - DO NOT REMOVE THIS LINE # # This file may be overwritten on upgrades. # @@ -103,6 +103,12 @@ KrbConstrainedDelegationLock ipa Allow from all </Location> +<Location "/ipa/session/sync_token"> + Satisfy Any + Order Deny,Allow + Allow from all +</Location> + # This is where we redirect on failed auth Alias /ipa/errors "/usr/share/ipa/html" diff --git a/ipaserver/plugins/ldap2.py b/ipaserver/plugins/ldap2.py index 29bb20d41..9ecd0b87c 100644 --- a/ipaserver/plugins/ldap2.py +++ b/ipaserver/plugins/ldap2.py @@ -93,7 +93,7 @@ class ldap2(LDAPClient, CrudBackend): def create_connection(self, ccache=None, bind_dn=None, bind_pw='', tls_cacertfile=None, tls_certfile=None, tls_keyfile=None, - debug_level=0, autobind=False): + debug_level=0, autobind=False, serverctrls=None, clientctrls=None): """ Connect to LDAP server. @@ -151,16 +151,22 @@ class ldap2(LDAPClient, CrudBackend): context=krbV.default_context()).principal().name os.environ['KRB5CCNAME'] = ccache - conn.sasl_interactive_bind_s(None, SASL_GSSAPI) + conn.sasl_interactive_bind_s(None, SASL_GSSAPI, + serverctrls=serverctrls, + clientctrls=clientctrls) setattr(context, 'principal', principal) else: # no kerberos ccache, use simple bind or external sasl if autobind: pent = pwd.getpwuid(os.geteuid()) auth_tokens = _ldap.sasl.external(pent.pw_name) - conn.sasl_interactive_bind_s(None, auth_tokens) + conn.sasl_interactive_bind_s(None, auth_tokens, + serverctrls=serverctrls, + clientctrls=clientctrls) else: - conn.simple_bind_s(bind_dn, bind_pw) + conn.simple_bind_s(bind_dn, bind_pw, + serverctrls=serverctrls, + clientctrls=clientctrls) return conn diff --git a/ipaserver/plugins/xmlserver.py b/ipaserver/plugins/xmlserver.py index 8d96262cf..7460ead69 100644 --- a/ipaserver/plugins/xmlserver.py +++ b/ipaserver/plugins/xmlserver.py @@ -25,7 +25,7 @@ Loads WSGI server plugins. from ipalib import api if 'in_server' in api.env and api.env.in_server is True: - from ipaserver.rpcserver import wsgi_dispatch, xmlserver, jsonserver_kerb, jsonserver_session, login_kerberos, login_password, change_password, xmlserver_session + from ipaserver.rpcserver import wsgi_dispatch, xmlserver, jsonserver_kerb, jsonserver_session, login_kerberos, login_password, change_password, sync_token, xmlserver_session api.register(wsgi_dispatch) api.register(xmlserver) api.register(jsonserver_kerb) @@ -33,4 +33,5 @@ if 'in_server' in api.env and api.env.in_server is True: api.register(login_kerberos) api.register(login_password) api.register(change_password) + api.register(sync_token) api.register(xmlserver_session) diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py index 0838ad143..454a53451 100644 --- a/ipaserver/rpcserver.py +++ b/ipaserver/rpcserver.py @@ -30,6 +30,10 @@ import datetime import urlparse import json +import ldap.controls +from pyasn1.type import univ, namedtype +from pyasn1.codec.ber import encoder + from ipalib import plugable, errors from ipalib.capabilities import VERSION_WITHOUT_CAPABILITIES from ipalib.backend import Executioner @@ -106,7 +110,7 @@ _unauthorized_template = """<html> </body> </html>""" -_pwchange_template = """<html> +_success_template = """<html> <head> <title>200 Success</title> </head> @@ -1105,10 +1109,110 @@ class change_password(Backend, HTTP_Status): response_headers.append(('X-IPA-Pwchange-Policy-Error', policy_error)) start_response(status, response_headers) - output = _pwchange_template % dict(title=str(title), - message=str(message)) + output = _success_template % dict(title=str(title), + message=str(message)) return [output] +class sync_token(Backend, HTTP_Status): + content_type = 'text/plain' + key = '/session/sync_token' + + class OTPSyncRequest(univ.Sequence): + OID = "2.16.840.1.113730.3.8.10.6" + + componentType = namedtype.NamedTypes( + namedtype.NamedType('firstCode', univ.OctetString()), + namedtype.NamedType('secondCode', univ.OctetString()), + namedtype.OptionalNamedType('tokenDN', univ.OctetString()) + ) + + def __init__(self): + super(sync_token, self).__init__() + + def _on_finalize(self): + super(sync_token, self)._on_finalize() + self.api.Backend.wsgi_dispatch.mount(self, self.key) + + def __call__(self, environ, start_response): + # Make sure this is a form request. + content_type = environ.get('CONTENT_TYPE', '').lower() + if not content_type.startswith('application/x-www-form-urlencoded'): + return self.bad_request(environ, start_response, "Content-Type must be application/x-www-form-urlencoded") + + # Make sure this is a POST request. + method = environ.get('REQUEST_METHOD', '').upper() + if method == 'POST': + query_string = read_input(environ) + else: + return self.bad_request(environ, start_response, "HTTP request method must be POST") + + # Parse the query string to a dictionary. + try: + query_dict = urlparse.parse_qs(query_string) + except Exception, e: + return self.bad_request(environ, start_response, "cannot parse query data") + data = {} + for field in ('user', 'password', 'first_code', 'second_code', 'token'): + value = query_dict.get(field, None) + if value is not None: + if len(value) == 1: + data[field] = value[0] + else: + return self.bad_request(environ, start_response, "more than one %s parameter" + % field) + elif field != 'token': + return self.bad_request(environ, start_response, "no %s specified" % field) + + # Create the request control. + sr = self.OTPSyncRequest() + sr.setComponentByName('firstCode', data['first_code']) + sr.setComponentByName('secondCode', data['second_code']) + if 'token' in data: + try: + token_dn = DN(data['token']) + except ValueError: + token_dn = DN((self.api.Object.otptoken.primary_key.name, data['token']), + self.api.env.container_otp, self.api.env.basedn) + + sr.setComponentByName('tokenDN', str(token_dn)) + rc = ldap.controls.RequestControl(sr.OID, True, encoder.encode(sr)) + + # Resolve the user DN + bind_dn = DN((self.api.Object.user.primary_key.name, data['user']), + self.api.env.container_user, self.api.env.basedn) + + # Start building the response. + status = HTTP_STATUS_SUCCESS + response_headers = [('Content-Type', 'text/html; charset=utf-8')] + title = 'Token sync rejected' + + # Perform the synchronization. + conn = ldap2(shared_instance=False, ldap_uri=self.api.env.ldap_uri) + try: + conn.connect(bind_dn=bind_dn, + bind_pw=data['password'], + serverctrls=[rc,]) + result = 'ok' + title = "Token sync successful" + message = "Token was synchronized." + except (NotFound, ACIError): + result = 'invalid-credentials' + message = 'The username, password or token codes are not correct.' + except Exception, e: + result = 'error' + message = "Could not connect to LDAP server." + self.error("token_sync: cannot authenticate '%s' to LDAP server: %s", + data['user'], str(e)) + finally: + if conn.isconnected(): + conn.destroy_connection() + + # Report status and return. + response_headers.append(('X-IPA-TokenSync-Result', result)) + start_response(status, response_headers) + output = _success_template % dict(title=str(title), + message=str(message)) + return [output] class xmlserver_session(xmlserver, KerberosSession): """ |