From 7662aecceb758e3ddc974943429f5c3740b47651 Mon Sep 17 00:00:00 2001
From: Branson Stephens <branson.stephens@ligo.org>
Date: Thu, 2 Apr 2015 16:50:28 -0500
Subject: [PATCH] Basic auth path and backend for LV-EM folks.

---
 gracedb/permission_utils.py            | 14 ++++++
 ligoauth/middleware/auth.py            | 67 +++++++++++++++++++++++++-
 settings/branson.py                    |  5 ++
 settings/default.py                    |  4 ++
 templates/profile/manage_password.html | 28 +++++++++++
 templates/profile/notifications.html   | 12 +++--
 urls.py                                |  1 +
 userprofile/urls.py                    | 12 ++---
 userprofile/views.py                   | 24 +++++++--
 9 files changed, 150 insertions(+), 17 deletions(-)
 create mode 100644 templates/profile/manage_password.html

diff --git a/gracedb/permission_utils.py b/gracedb/permission_utils.py
index 5e6633fd9..b7ba9fdf3 100644
--- a/gracedb/permission_utils.py
+++ b/gracedb/permission_utils.py
@@ -59,3 +59,17 @@ def internal_user_required(view):
             return HttpResponseForbidden("Forbidden")
         return view(request, *args, **kwargs)
     return inner
+
+#-------------------------------------------------------------------------------
+# A wrapper for views that checks whether the user is in the LV-EM group, and if not
+# returns a 403.
+#-------------------------------------------------------------------------------
+def lvem_user_required(view):
+    @wraps(view)
+    def inner(request, *args, **kwargs):
+        # XXX Should probably move this list of internal groups into settings.
+        lvem_groups = [Group.objects.get(name='gw-astronomy:LV-EM')]
+        if not set(list(lvem_groups)) & set(list(request.user.groups.all())):
+            return HttpResponseForbidden("Forbidden")
+        return view(request, *args, **kwargs)
+    return inner
diff --git a/ligoauth/middleware/auth.py b/ligoauth/middleware/auth.py
index f30c74ccb..272d543bb 100644
--- a/ligoauth/middleware/auth.py
+++ b/ligoauth/middleware/auth.py
@@ -1,5 +1,6 @@
 
 import re
+from django.conf import settings
 from django.contrib.auth import authenticate
 from django.contrib.auth.models import User, AnonymousUser, Group
 from django.contrib.auth.backends import RemoteUserBackend as DefaultRemoteUserBackend
@@ -9,10 +10,14 @@ from ligoauth.models import certdn_to_user
 from django.shortcuts import render_to_response
 from django.template import RequestContext
 
-from django.http import HttpResponseForbidden
+from django.http import HttpResponse, HttpResponseForbidden
 
 proxyPattern = re.compile(r'^(.*?)(/CN=\d+)*$')
 
+from datetime import datetime
+from base64 import b64decode
+import json
+
 # XXX Hack. This will go away when we get the new perms infrastructure in place.
 PUBLIC_URLS = [
     '/',
@@ -108,6 +113,19 @@ class LigoAuthMiddleware:
         if not user and dn:
             user = authenticate(dn=dn)
 
+        authn_header = request.META.get('HTTP_AUTHORIZATION', None)
+        if not user and 'apibasic' in request.path and authn_header:
+            user = authenticate(authn_header=authn_header)
+            # XXX Note: We are using date_joined to store the date
+            # when the password was set, not when the user joined up. 
+            # This could cause some strange behavior if we ever want to
+            # actually use 'date_joined' for it's intended purpose.
+            # check: is now greater than date_joined + time_delta?
+            if user:
+                if datetime.now() > user.date_joined + settings.PASSWORD_EXPIRATION_TIME:
+                    msg = "Your password has expired. Please log in and request another."
+                    return HttpResponseForbidden(json.dumps({'error': msg})) 
+
         if user and user.is_active:
             # Ideal, normal case.  Yay!
             pass
@@ -134,6 +152,12 @@ class LigoAuthMiddleware:
             if is_cli:
                 message = "Your credentials are not valid."
                 return HttpResponseForbidden("{ 'error': '%s'  }" % message)
+            if 'apibasic' in request.path:
+                # The user was trying to get to the API exposed by basic auth. Send JSON with challenge.
+                msg = "Login failed: Incorrect username or password."
+                response = HttpResponse(json.dumps({'error': msg}), status=401)
+                response['WWW-Authenticate'] = 'Basic realm="/apibasic/"'
+                return response
             return render_to_response(
                     'forbidden.html',
                     {'error': message},
@@ -175,6 +199,47 @@ class LigoShibBackend:
         except User.DoesNotExist:
             return None
 
+class LigoBasicBackend:
+    
+    supports_object_permissions = False
+    supports_anonymous_user = False
+    supports_inactive_user = False
+
+    def authenticate(self, authn_header):
+        # dig out the username and password from the authn header
+        username = None
+        password = None
+
+        try:
+            authn_type, cred = authn_header.strip().split()
+        except:
+            return None
+
+        if authn_type.lower() != 'basic':
+            return None
+
+        try:
+            cred = b64decode(cred)
+            username, password = cred.split(':')
+        except:
+            return None
+
+        try:
+            user = User.objects.get(username=username)
+        except User.DoesNotExist:
+            return None
+
+        if user.check_password(password):
+            return user
+        else:
+            return None
+
+    def get_user(self, user_id):
+        try:
+            return User.objects.get(id=user_id)
+        except User.DoesNotExist:
+            return None
+
 class ModelBackend(DefaultModelBackend):
     def authenticate(self, username=None, password=None, **kwargs):
         return None
diff --git a/settings/branson.py b/settings/branson.py
index 8bfb036fb..08c81e981 100644
--- a/settings/branson.py
+++ b/settings/branson.py
@@ -229,6 +229,11 @@ LOGGING = {
             'propagate': True,
             'level': LOG_LEVEL,
         },
+        'ligoauth': {
+            'handlers': ['debug_file'],
+            'propagate': True,
+            'level': LOG_LEVEL,
+        },
         'middleware': {
             'handlers': ['performance_file'],
             'propagate': True,
diff --git a/settings/default.py b/settings/default.py
index 88769f0ce..fee4bad5c 100644
--- a/settings/default.py
+++ b/settings/default.py
@@ -231,6 +231,7 @@ AUTHENTICATION_BACKENDS = (
 #   'gracedb.middleware.auth.LigoAuthBackend',
     'ligoauth.middleware.auth.LigoX509Backend',
     'ligoauth.middleware.auth.LigoShibBackend',
+    'ligoauth.middleware.auth.LigoBasicBackend',
     'ligoauth.middleware.auth.ModelBackend',
 #   'ligoauth.middleware.auth.RemoteUserBackend',
 #   'ligodjangoauth.LigoShibbolethAuthBackend',
@@ -315,6 +316,9 @@ SOUTH_MIGRATION_MODULES = {
 
 SOUTH_TESTS_MIGRATE = False
 
+# passwords for LVEM scripted access expire after 365 days.
+PASSWORD_EXPIRATION_TIME = timedelta(days=365)
+
 # XXX The following Log settings are for a performance metric.
 import logging
 LOG_ROOT = '/home/gracedb/logs'
diff --git a/templates/profile/manage_password.html b/templates/profile/manage_password.html
new file mode 100644
index 000000000..7809ef94c
--- /dev/null
+++ b/templates/profile/manage_password.html
@@ -0,0 +1,28 @@
+{% extends "base.html" %}
+
+{% block title %}Options | Password Manager{% endblock %}
+{% block heading %}Password Manager{% endblock %}
+{% block pageid %}userprofile{% endblock %}
+
+{% block content %}
+
+<p> Passwords generated here are intended only for scripted access to GraceDB by LV-EM users. Your password allows access to the <a href={% url "basic:api-root" %}>REST API</a>. </p>
+
+<p> Your username is: <span style="color: red"> {{ username }} </span> </p>
+
+{% if password %}
+<p> Your password is: <span style="color: red"> {{ password }} </span> </p>
+{% endif %}
+
+<br/>
+
+<p> Press the button here to get a new password (or change your existing one): </p>
+
+<form action={% url "userprofile-manage-password" %} method="post">
+    <input type="submit" value="Get me a password!">
+</form>
+
+<p> <b>Note:</b> Clicking this button has the effect of changing your password, and any old 
+passwords will no longer work. Also, passwords will expire after one year. </p>
+
+{% endblock %}
diff --git a/templates/profile/notifications.html b/templates/profile/notifications.html
index 100bb45ce..225489f46 100644
--- a/templates/profile/notifications.html
+++ b/templates/profile/notifications.html
@@ -1,7 +1,7 @@
 {% extends "base.html" %}
 
 {% block title %}Options | Notifications{% endblock %}
-{% block heading %}Notifications{% endblock %}
+{% block heading %}Notifications (LVC Users){% endblock %}
 {% block pageid %}userprofile{% endblock %}
 
 {% block content %}
@@ -16,9 +16,10 @@
    </ul>
 {% endfor %}
 
-<a href="{% url "userprofile-create" %}">Create New Notification</a>
+<a href="{% url "userprofile-create" %}">Create New Notification (LVC users)</a>
+<br/><br/>
 
-<h2>Contacts</h2>
+<h2>Contacts (LVC Users)</h2>
 {% for contact in contacts %}
     <ul>
         <li>
@@ -30,5 +31,10 @@
 {% endfor %}
 
 <a href="{% url "userprofile-create-contact" %}">Create New Contact</a>
+<br/><br/>
+
+<h2>Passwords for Scripted Access (LV-EM users)</h2>
+
+<a href="{% url "userprofile-manage-password" %}">Manage Password</a>
 
 {% endblock %}
diff --git a/urls.py b/urls.py
index 2c89360c7..e20f56265 100644
--- a/urls.py
+++ b/urls.py
@@ -23,6 +23,7 @@ urlpatterns = patterns('',
     (r'^events/', include('gracedb.urls')),
     (r'^api/',    include('gracedb.urls_rest', app_name="api", namespace="x509")),
     (r'^apiweb/', include('gracedb.urls_rest', app_name="api", namespace="shib")),
+    (r'^apibasic/', include('gracedb.urls_rest', app_name="api", namespace="basic")),
     (r'^options/', include('userprofile.urls')),
     (r'^cli/create', 'gracedb.views.create'),
     (r'^cli/ping', 'gracedb.cli_views.ping'),
diff --git a/userprofile/urls.py b/userprofile/urls.py
index 48f928778..21d3bd4c5 100644
--- a/userprofile/urls.py
+++ b/userprofile/urls.py
@@ -1,8 +1,7 @@
 
 # Changed for Django 1.6
 #from django.conf.urls.defaults import *
-from django.conf.urls import patterns, url, include
-
+from django.conf.urls import patterns, url
 
 urlpatterns = patterns('userprofile.views',
     url (r'^$', 'index', name="userprofile-home"),
@@ -14,11 +13,6 @@ urlpatterns = patterns('userprofile.views',
     url (r'^trigger/delete/(?P<id>[\d]+)$', 'delete', name="userprofile-delete"),
     url (r'^trigger/edit/(?P<id>[\d]+)$', 'edit', name="userprofile-edit"),
 
-#   (r'^view/(?P<uid>[\w\d]+)', 'view'),
-#   (r'^edit/(?P<uid>[\w\d]+)', 'edit'),
-#   (r'^request_archive/(?P<uid>[\w\d]+)(?P<rescind>/rescind)?', 'request_archive'),
-#   (r'^approve_archive/(?P<uid>[\w\d]+)(?P<rescind>/rescind)?', 'approve_archive'),
-#   url (r'^query', 'query', name="search"),
-#   url (r'^mine/$', 'mine', name="mine"),
-#   url (r'^myapprovals/$', 'myapprovals', name="myapprovals"),
+    url (r'^manage_password$', 'managePassword', name="userprofile-manage-password"),
+
 )
diff --git a/userprofile/views.py b/userprofile/views.py
index dd23c7bcb..ee7a2cd09 100644
--- a/userprofile/views.py
+++ b/userprofile/views.py
@@ -3,8 +3,8 @@ from django.http import HttpResponse
 from django.http import HttpResponseRedirect, HttpResponseNotFound
 from django.http import Http404, HttpResponseForbidden
 
-from django.core.urlresolvers import reverse
-
+from django.core.urlresolvers import reverse 
+from django.contrib.auth.models import User
 from django.template import RequestContext
 from django.shortcuts import render_to_response
 
@@ -12,9 +12,12 @@ from models import Trigger, Contact
 
 from forms import ContactForm, triggerFormFactory
 
-from gracedb.permission_utils import internal_user_required
+from gracedb.permission_utils import internal_user_required, lvem_user_required
 
-@internal_user_required
+from datetime import datetime
+
+# Let's let everybody onto the index view.
+#@internal_user_required
 def index(request):
     triggers = Trigger.objects.filter(user=request.user)
     contacts = Contact.objects.filter(user=request.user)
@@ -23,6 +26,19 @@ def index(request):
                               d,
                               context_instance=RequestContext(request))
 
+@lvem_user_required
+def managePassword(request):
+    d = { 'username': request.user.username }
+    if request.method == "POST":
+        password = User.objects.make_random_password(length=20)
+        d['password'] = password
+        request.user.set_password(password)
+        request.user.date_joined = datetime.now()
+        request.user.save()
+    return render_to_response('profile/manage_password.html',
+                              d,
+                              context_instance=RequestContext(request))
+
 @internal_user_required
 def create(request):
     explanation = ""
-- 
GitLab