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