diff options
author | Nathaniel McCallum <npmccallum@redhat.com> | 2014-05-28 11:38:40 -0400 |
---|---|---|
committer | Martin Kosek <mkosek@redhat.com> | 2014-06-26 15:55:24 +0200 |
commit | 14b38b7704778b4000a7b1b31d78fbb6b45e647b (patch) | |
tree | 13408e8d9f6204e7f311592bd4c1ab7f284a6744 /ipaserver | |
parent | 1c94edd3a09711d85ba099bd815c0bdd8f0210c1 (diff) | |
download | freeipa-14b38b7704778b4000a7b1b31d78fbb6b45e647b.tar.gz freeipa-14b38b7704778b4000a7b1b31d78fbb6b45e647b.tar.xz freeipa-14b38b7704778b4000a7b1b31d78fbb6b45e647b.zip |
Add /session/token_sync POST support
This HTTP call takes the following parameters:
* user
* password
* first_code
* second_code
* token (optional)
Using this information, the server will perform token synchronization.
If the token is not specified, all tokens will be searched for synchronization.
Otherwise, only the token specified will be searched.
https://fedorahosted.org/freeipa/ticket/4218
Reviewed-By: Alexander Bokovoy <abokovoy@redhat.com>
Diffstat (limited to 'ipaserver')
-rw-r--r-- | ipaserver/plugins/ldap2.py | 14 | ||||
-rw-r--r-- | ipaserver/plugins/xmlserver.py | 3 | ||||
-rw-r--r-- | ipaserver/rpcserver.py | 110 |
3 files changed, 119 insertions, 8 deletions
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): """ |