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

Reply via email to