You can use the attached script (changepw.py) to test the PW change interface from command line (on IPA server).
--- IPA server web form-based authentication allows logins for users which for some reason cannot use Kerberos authentication. However, when a password for such users expires, they are unable change the password via web interface. This patch adds a new WSGI script attached to URL /ipa/session/change_password which can be accessed without authentication and which provides password change capability for web services. The actual password change in the script is processed with kpasswd to be consistent with /ipa/session/login_password. Password result is passed both in the resulting HTML page, but also in HTTP headers for easier parsing in web services: X-IPA-Pwchange-Result: {ok, invalid-password, policy-error} (optional) X-IPA-Pwchange-Policy-Error: $policy_error_text https://fedorahosted.org/freeipa/ticket/2276
>From a30303b5f3098d745bacf7c6a6e4a836e3e231d2 Mon Sep 17 00:00:00 2001 From: Martin Kosek <mko...@redhat.com> Date: Wed, 6 Jun 2012 14:38:08 +0200 Subject: [PATCH] Password change capability for form-based auth IPA server web form-based authentication allows logins for users which for some reason cannot use Kerberos authentication. However, when a password for such users expires, they are unable change the password via web interface. This patch adds a new WSGI script attached to URL /ipa/session/change_password which can be accessed without authentication and which provides password change capability for web services. The actual password change in the script is processed with kpasswd to be consistent with /ipa/session/login_password. Password result is passed both in the resulting HTML page, but also in HTTP headers for easier parsing in web services: X-IPA-Pwchange-Result: {ok, invalid-password, policy-error} (optional) X-IPA-Pwchange-Policy-Error: $policy_error_text https://fedorahosted.org/freeipa/ticket/2276 --- install/conf/ipa.conf | 8 ++- ipaserver/plugins/xmlserver.py | 3 +- ipaserver/rpcserver.py | 169 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 178 insertions(+), 2 deletions(-) diff --git a/install/conf/ipa.conf b/install/conf/ipa.conf index 89c9849ca6656ae3da585a72392d5d2463f4d892..b52d9d2ff722c77c37619cc4a4c0fb7cebd5354f 100644 --- a/install/conf/ipa.conf +++ b/install/conf/ipa.conf @@ -1,5 +1,5 @@ # -# VERSION 4 - DO NOT REMOVE THIS LINE +# VERSION 5 - DO NOT REMOVE THIS LINE # # LoadModule auth_kerb_module modules/mod_auth_kerb.so @@ -72,6 +72,12 @@ KrbConstrainedDelegationLock ipa Allow from all </Location> +<Location "/ipa/session/change_password"> + 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/xmlserver.py b/ipaserver/plugins/xmlserver.py index 4ae914950b3dc4f593f03853dc1450d1dfea78d5..bd9eb1fdf72a1b8f5cab727d63070de377df07c9 100644 --- a/ipaserver/plugins/xmlserver.py +++ b/ipaserver/plugins/xmlserver.py @@ -25,10 +25,11 @@ 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 + from ipaserver.rpcserver import wsgi_dispatch, xmlserver, jsonserver_kerb, jsonserver_session, login_kerberos, login_password, change_password api.register(wsgi_dispatch) api.register(xmlserver) api.register(jsonserver_kerb) api.register(jsonserver_session) api.register(login_kerberos) api.register(login_password) + api.register(change_password) diff --git a/ipaserver/rpcserver.py b/ipaserver/rpcserver.py index f9a549f4e8e0135e68c3a2ae8a9e876cac0d88df..2f639f229c7be53bf157d97f4c209ad449268bea 100644 --- a/ipaserver/rpcserver.py +++ b/ipaserver/rpcserver.py @@ -100,6 +100,18 @@ _unauthorized_template = """<html> </body> </html>""" +_pwchange_template = """<html> +<head> +<title>200 Success</title> +</head> +<body> +<h1>%(title)s</h1> +<p> +<strong>%(message)s</strong> +</p> +</body> +</html>""" + class HTTP_Status(plugable.Plugin): def not_found(self, environ, start_response, url, message): """ @@ -992,3 +1004,160 @@ class login_password(Backend, KerberosSession, HTTP_Status): if returncode != 0: raise InvalidSessionPassword(principal=principal, message=unicode(stderr)) +class KpasswdError(StandardError): + def __init__(self, error_message, principal): + self.error_message = error_message + self.principal = principal + +class PasswordPolicyError(KpasswdError): + def __init__(self, error_message, principal, rejection_reason): + super(PasswordPolicyError, self).__init__(error_message, principal) + self.rejection_reason = rejection_reason + +class InvalidPasswordError(KpasswdError): + pass + +class change_password(Backend, HTTP_Status): + + content_type = 'text/plain' + key = '/session/change_password' + + def __init__(self): + super(change_password, self).__init__() + + def _on_finalize(self): + super(change_password, self)._on_finalize() + self.api.Backend.wsgi_dispatch.mount(self, self.key) + + def __call__(self, environ, start_response): + self.debug('WSGI change_password.__call__:') + + # Get the user and password parameters from the 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") + + 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") + + 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', 'old_password', 'new_password'): + 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) + else: + return self.bad_request(environ, start_response, "no %s specified" % field) + + # start building the response + status = '200 Success' + response_headers = [('Content-Type', 'text/html; charset=utf-8')] + title = 'Password change rejected' + result = 'invalid-error' + policy_error = None + + # run kpasswd to change the password + try: + self.kpasswd(data['user'], self.api.env.realm, data['old_password'], + data['new_password']) + except PasswordPolicyError, e: + message = 'New password is not compliant with Kerberos password policy:' \ + '<br>\n%(message)s' \ + % dict(principal=escape(e.principal), + message=escape(e.error_message)) + result = 'policy-error' + policy_error = escape(str(e.rejection_reason)) + except (InvalidPasswordError, KpasswdError), e: + # report the same error message for general error and invalid password error + message = 'Principal %(principal)s cannot be authenticated:' \ + '<br>\n%(message)s' \ + % dict(principal=escape(e.principal), + message=escape(e.error_message)) + result = 'invalid-password' + else: + title = "Password change successful" + message = "Password has been changed." + result = 'ok' + + self.info('%s: %s', status, message) + + response_headers.append(('X-IPA-Pwchange-Result', result)) + if policy_error: + 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)) + return [output] + + def kpasswd(self, user, realm, old_password, new_password): + """ + Call kpasswd binary to change the password + + :param user User to be logged in + :param realm Kerberos realm + :param old_password Old password + :param new_password New password + """ + + # Format the user as a kerberos principal + principal = krb5_format_principal_name(user, realm) + + stdin = "%(old_password)s\n%(new_password)s\n%(new_password)s" \ + % dict(old_password=old_password, new_password=new_password) + + (stdout, stderr, returncode) = ipautil.run(['/usr/bin/kpasswd', principal], + stdin=stdin, raiseonerr=False) + + self.debug('kpasswd: principal=%s returncode=%s, stderr="%s"', + principal, returncode, stderr) + + if returncode == 0: + # password has been changed + return + elif returncode == 1: + # 1: invalid password error (invalid password or locked account) + raise InvalidPasswordError(error_message=unicode(stderr), + principal=principal) + elif returncode == 2: + # 2: password policy error + # error message is now in stdout, not stderr + # stdout structure: + # Password for $principal: + # Enter new password: + # Enter it again: + # Password change rejected: $reason <<< + reason = "Unknown reason" + error_message = stdout + + # try to parse policy error + try: + lines = error_message.split("\n") + label,colon,reason = lines[3].partition(':') + reason = reason.strip() + except Exception, e: + self.debug('could not parse reason from kpasswd output: %s', e) + + # try to parse the error message line out + try: + error_message = lines[3].strip() + except Exception, e: + self.debug('could not parse error message from kpasswd output: %s', e) + + raise PasswordPolicyError(error_message=unicode(error_message), + principal=principal, + rejection_reason=reason) + else: + raise KpasswdError(error_message=unicode(stderr), + principal=principal) -- 1.7.7.6
#!/usr/bin/python import socket import sys import pycurl import urllib DEBUG=True def change_password(hostname, user, old_password, new_password): url = 'https://%s/ipa/session/change_password' % hostname print "Perform password change on the IPA server URL: %s", url print "Change password for '%s'from '%s' to '%s'" % (user, old_password, new_password) request = { 'user': user, 'old_password': old_password, 'new_password': new_password, } request_data = urllib.urlencode(request, True) c = pycurl.Curl() c.setopt(pycurl.URL, url) c.setopt(pycurl.HTTPHEADER, [ "Content-Type: application/x-www-form-urlencoded", "Referer: %s" % url ] ) #set POST fields c.setopt(pycurl.POST, 1) c.setopt(pycurl.POSTFIELDS, request_data) c.setopt(pycurl.SSL_VERIFYPEER, False) if DEBUG: c.setopt(pycurl.VERBOSE, 1) c.perform() if __name__ == "__main__": if len(sys.argv) != 4: sys.exit('Usage: ./changepw.py USER OLD_PASSWORD NEW_PASSWORD') user=sys.argv[1] old_password=sys.argv[2] new_password=sys.argv[3] hostname=socket.gethostname() # use current hostname change_password(hostname, user, old_password, new_password)
_______________________________________________ Freeipa-devel mailing list Freeipa-devel@redhat.com https://www.redhat.com/mailman/listinfo/freeipa-devel