pvl.login: implement LDAPAuth; fix Index pageprogress grace period refresh
authorTero Marttila <terom@paivola.fi>
Mon, 13 Jan 2014 20:25:36 +0200
changeset 367 e431a1b71006
parent 366 af3833864b89
child 368 be42e2d38c77
pvl.login: implement LDAPAuth; fix Index pageprogress grace period refresh
bin/pvl.login-server
pvl/login/auth.py
pvl/login/server.py
pvl/login/static/pubtkt-expire.js
--- a/bin/pvl.login-server	Mon Jan 13 20:25:03 2014 +0200
+++ b/bin/pvl.login-server	Mon Jan 13 20:25:36 2014 +0200
@@ -6,6 +6,8 @@
 
 
 import pvl.args
+import pvl.ldap.args
+import pvl.login.auth
 import pvl.login.server
 import pvl.web.args
 
@@ -22,13 +24,18 @@
     parser = optparse.OptionParser(main.__doc__)
     parser.add_option_group(pvl.args.parser(parser))
     parser.add_option_group(pvl.web.args.parser(parser))
+    parser.add_option_group(pvl.ldap.args.parser(parser))
 
     options, args = parser.parse_args(argv[1:])
     pvl.args.apply(options)
 
+    # ldap
+    ldap = pvl.ldap.args.apply(options)
+
     # app
     application = pvl.web.args.apply(options,
             pvl.login.server.LoginApplication,
+            auth=pvl.login.auth.LDAPAuth(ldap),
     )
 
     # behind a reverse-proxy
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/pvl/login/auth.py	Mon Jan 13 20:25:36 2014 +0200
@@ -0,0 +1,75 @@
+import ldap
+
+import pvl.ldap.domain
+
+import logging; log = logging.getLogger('pvl.login.auth')
+
+class AuthError (Exception) :
+    def __init__ (self, error) :
+        self.error = error
+
+    def __unicode__ (self) :
+        return u"Authenticating against the backend failed: {self.error}".format(self=self)
+
+class LDAPAuth (object) :
+    def __init__ (self, ldap) :
+        self.ldap = ldap
+
+    def auth (self, username, password) :
+        """
+            Attempt to bind against LDAP with given user object and password.
+
+            Returns None if the user does not seem to exist, False on invalid auth, True on valid auth.
+
+            Raises AuthError.
+        """
+        
+        # search
+        try :
+            user = self.ldap.users.get(username)
+        except KeyError :
+            log.info("%s: not found", username)
+            return None
+        else :
+            log.info("%s: %s", username, user)
+        
+        # bind
+        bind = self.bind(user, password)
+        
+        if bind :
+            return True
+        else :
+            return False
+
+    def bind (self, user, password) :
+        """
+            Attempt to bind against LDAP with given user object and password.
+        
+            Returns the bound connection, or
+                None        - if the user does not seem toe xist
+                False       - invalid auth
+
+            Raises AuthError.
+        """
+
+        conn = self.ldap.open()
+        
+        try :
+            conn.bind(user.dn, password)
+
+        except ldap.INVALID_CREDENTIALS as ex :
+            log.info("%s: INVALID_CREDENTIALS", user)
+            return False
+
+        except ldap.NO_SUCH_OBJECT as ex :
+            log.info("%s: ldap.NO_SUCH_OBJECT", user)
+            return None
+    
+        except ldap.LDAPError as ex :
+            log.exception("%s", user)
+            raise AuthError(ex)
+
+        else :
+            log.info("%s", user)
+            return conn
+
--- a/pvl/login/server.py	Mon Jan 13 20:25:03 2014 +0200
+++ b/pvl/login/server.py	Mon Jan 13 20:25:36 2014 +0200
@@ -5,6 +5,7 @@
 import werkzeug
 import werkzeug.urls
 
+import pvl.login.auth
 import pvl.web
 import pvl.web.response
 
@@ -185,6 +186,7 @@
         lifetime = self.app.login_valid
         valid = pubtkt.valid()
         grace = pubtkt.grace()
+        grace_period = pubtkt.grace_period()
         remaining = pubtkt.remaining()
 
         if valid :
@@ -210,7 +212,7 @@
                 html.span(class_='glyphicon glyphicon-time'),
                 html.div(class_='progress pubtkt-progress',
                     data_start=valid.seconds,
-                    data_refresh=remaining.seconds if remaining else None,
+                    data_refresh=grace_period.seconds if remaining else None,
                     data_end=lifetime.seconds,
                 )(
                     html.div(class_='progress-bar progress-bar-{status}'.format(status=status),
@@ -307,23 +309,34 @@
             self.process_back()
         except pubtkt.Error as ex :
             self.alert('danger', ex)
+
+        if self.pubtkt :
+            self.username = self.pubtkt.uid
+        else :
+            self.username = None
             
         # update cookie?
         set_pubtkt = None
 
         if self.request.method == 'POST' :
             username = self.request.form.get('username')
-            password = self.request.form.get('username')
+            password = self.request.form.get('password')
+                
+            if username :
+                # preprocess
+                username = username.strip().lower()
 
             if username and password :
-                # preprocess
-                username = username.strip().lower()
+                self.username = username
                 
                 try :
                     set_pubtkt = self.app.auth(username, password)
 
-                except pubtkt.Error as ex :
-                    self.auth_errors = ex
+                except pvl.login.auth.AuthError as ex :
+                    self.alert('danger', "Internal authentication error, try again later?")
+
+                if not set_pubtkt :
+                    self.alert('danger', "Invalid authentication credentials, try again.")
             
             elif self.pubtkt and self.pubtkt.valid() :
                 # renew manually if valid
@@ -354,11 +367,6 @@
 
 
     def render (self) :
-        if self.pubtkt :
-            username = self.pubtkt.uid
-        else :
-            username = None
-
         domain = self.app.login_domain
 
         if 'logout' in self.request.args :
@@ -390,12 +398,12 @@
                     html.div(class_='form-group')(
                         html.div(class_='input-group')(
                             html.label(for_='username', class_='sr-only')("Username"),
-                            html.input(name='username', type='text', class_='form-control', placeholder="username", required=True, autofocus=True, value=username),
+                            html.input(name='username', type='text', class_='form-control', placeholder="username", required=True, autofocus=(not self.username), value=self.username),
                             html.span(class_='input-group-addon')("@{domain}".format(domain=domain)),
                         ),
 
                         html.label(for_='password', class_='sr-only')("Password"),
-                        html.input(name='password', type='password', class_='form-control', placeholder="Password", required=(not renew)),
+                        html.input(name='password', type='password', class_='form-control', placeholder="Password", required=(not renew), autofocus=bool(self.username)),
                     ),
 
                     html.button(type='submit', class_='btn btn-primary')(
@@ -464,9 +472,10 @@
     cookie_secure = True
     cookie_httponly = True
 
-    def __init__ (self, public_key=PUBLIC_KEY, private_key=PRIVATE_KEY, **opts) :
+    def __init__ (self, auth, public_key=PUBLIC_KEY, private_key=PRIVATE_KEY, **opts) :
         super(LoginApplication, self).__init__(**opts)
         
+        self._auth = auth
         self.server_keys = pubtkt.ServerKeys.config(
                 public_key  = public_key,
                 private_key = private_key,
@@ -493,9 +502,16 @@
 
     def auth (self, username, password) :
         """
-            Perform authentication, returning a PubTkt, signed
+            Perform authentication, returning a PubTkt, signed, or None.
+
+            Raises auth.AuthError.
         """
+
+        auth = self._auth.auth(username, password)
         
+        if not auth :
+            return None
+
         return pubtkt.PubTkt.new(username,
                 valid   = self.login_valid,
                 grace   = self.login_grace,
--- a/pvl/login/static/pubtkt-expire.js	Mon Jan 13 20:25:03 2014 +0200
+++ b/pvl/login/static/pubtkt-expire.js	Mon Jan 13 20:25:36 2014 +0200
@@ -33,14 +33,21 @@
         
         var start_time = Date.now();
 
+        var reload = false;
+
         function update_progress () {
             var duration = (Date.now() - start_time) / 1000;
 
             var progress = progress_start - duration;
             
+            if (reload) {
+                // delayed reload
+                window.location.reload(true);
+            }
+            
             if (progress <= 0 || (progress_refresh && progress < progress_refresh)) {
                 // done
-                window.location.reload(true);
+                reload = true;
             } else {
                 // update
                 progress_bar.css('width', (progress * 100 / progress_end) + '%');