From c6c43c1689ab249705a1697ce2e2c18412d1f3a0 Mon Sep 17 00:00:00 2001 From: Magnus Hagander Date: Sat, 19 Jul 2014 15:10:49 +0200 Subject: [PATCH] Implement user searching in the community auth system This lets downstream systems securely search for users that are in the system, so they can populate their local database with users before they have logged in if necessary. This can be used for example for the commitfest management system to be able to flag users as authors and reviewers even before they have logged in. --- docs/authentication.rst | 37 +++++++++++++++++++++ pgweb/account/urls.py | 1 + pgweb/account/views.py | 39 ++++++++++++++++++++++- tools/communityauth/sample/django/auth.py | 37 ++++++++++++++++++++- 4 files changed, 112 insertions(+), 2 deletions(-) diff --git a/docs/authentication.rst b/docs/authentication.rst index d3e5110d..66fe44a6 100644 --- a/docs/authentication.rst +++ b/docs/authentication.rst @@ -143,3 +143,40 @@ The flow for a logout request is trivial: #. The main website redirects the user back to the community website, at the URL ?s=logout (where redirection_url is the same URL as when logging in) + +Searching +--------- +The community authentication system also supports an API for searching for +users. The idea here is to give the ability to add a user to downstream +systems even if that user has not yet logged in (normally the user is added +on first login). + +In order to not leak sensitive information about users, all search results +are returned encrypted with the same key as the authentication scheme. + +The flow for search is: + +#. Make an API call to + https://www.postgresql.org/account/auth//search/? + where the id is the same id as during login, and params can be one of + the following: + + s + Case insensitive substring search of name and email + n + Case insensitive substring search of name + e + Case insensitive substring search of email + u + Exact search of username + +#. The returned data will be an array of JSON objects, with the following keys: + + u + Username + e + Email + f + First name + l + Last name diff --git a/pgweb/account/urls.py b/pgweb/account/urls.py index 0dc7c8f0..92a764fb 100644 --- a/pgweb/account/urls.py +++ b/pgweb/account/urls.py @@ -7,6 +7,7 @@ urlpatterns = patterns('', # Community authenticatoin (r'^auth/(\d+)/$', 'account.views.communityauth'), (r'^auth/(\d+)/logout/$', 'account.views.communityauth_logout'), + (r'^auth/(\d+)/search/$', 'account.views.communityauth_search'), # Profile (r'^profile/$', 'account.views.profile'), diff --git a/pgweb/account/views.py b/pgweb/account/views.py index 707d046c..e4510347 100644 --- a/pgweb/account/views.py +++ b/pgweb/account/views.py @@ -1,6 +1,6 @@ from django.contrib.auth.models import User import django.contrib.auth.views as authviews -from django.http import HttpResponseRedirect, Http404 +from django.http import HttpResponseRedirect, Http404, HttpResponse from django.shortcuts import render_to_response, get_object_or_404 from django.contrib.auth.decorators import login_required from django.utils.http import int_to_base36 @@ -8,12 +8,14 @@ from django.contrib.auth.tokens import default_token_generator from django.contrib.auth import logout as django_logout from django.conf import settings from django.db import transaction +from django.db.models import Q import base64 import urllib from Crypto.Cipher import AES from Crypto import Random import time +import simplejson as json from pgweb.util.decorators import ssl_required from pgweb.util.contexts import NavContext @@ -322,3 +324,38 @@ def communityauth_logout(request, siteid): # Redirect user back to the specified suburl return HttpResponseRedirect("%s?s=logout" % site.redirecturl) + +@ssl_required +def communityauth_search(request, siteid): + # Perform a search for users. The response will be encrypted with the site + # key to prevent abuse, therefor we need the site. + site = get_object_or_404(CommunityAuthSite, pk=siteid) + + q = Q(is_active=True) + if request.GET.has_key('s') and request.GET['s']: + # General search term, match both name and email + q = q & (Q(email__icontains=request.GET['s']) | Q(first_name__icontains=request.GET['s']) | Q(last_name__icontains=request.GET['s'])) + elif request.GET.has_key('e') and request.GET['e']: + q = q & Q(email__icontains=request.GET['e']) + elif request.GET.has_key('n') and request.GET['n']: + q = q & (Q(first_name__icontains=request.GET['n']) | Q(last_name__icontains=request.GET['n'])) + elif request.GET.has_key('u') and request.GET['u']: + q = q & Q(username=request.GET['u']) + else: + raise Http404('No search term specified') + + users = User.objects.filter(q) + + j = json.dumps([{'u': u.username, 'e': u.email, 'f': u.first_name, 'l': u.last_name} for u in users]) + + # Encrypt it with the shared key (and IV!) + r = Random.new() + iv = r.read(16) # Always 16 bytes for AES + encryptor = AES.new(base64.b64decode(site.cryptkey), AES.MODE_CBC, iv) + cipher = encryptor.encrypt(j + ' ' * (16-(len(j) % 16))) #Pad to even 16 bytes + + # Base64-encode the response, just to be consistent + return HttpResponse("%s&%s" % ( + base64.b64encode(iv, '-_'), + base64.b64encode(cipher, '-_'), + )) diff --git a/tools/communityauth/sample/django/auth.py b/tools/communityauth/sample/django/auth.py index ecb3d486..769ebab8 100644 --- a/tools/communityauth/sample/django/auth.py +++ b/tools/communityauth/sample/django/auth.py @@ -6,7 +6,7 @@ # # To integrate with django, you need the following: # * Make sure the view "login" from this module is used for login -# * Map an url somwehere (typicall /auth_receive/) to the auth_receive +# * Map an url somwehere (typically /auth_receive/) to the auth_receive # view. # * In settings.py, set AUTHENTICATION_BACKENDS to point to the class # AuthBackend in this module. @@ -26,6 +26,8 @@ from django.contrib.auth import logout as django_logout from django.conf import settings import base64 +import simplejson +import socket import urlparse import urllib from Crypto.Cipher import AES @@ -168,3 +170,36 @@ We apologize for the inconvenience. if hasattr(settings, 'PGAUTH_REDIRECT_SUCCESS'): return HttpResponseRedirect(settings.PGAUTH_REDIRECT_SUCCESS) raise Exception("Authentication successful, but don't know where to redirect!") + + +# Perform a search in the central system. Note that the results are returned as an +# array of dicts, and *not* as User objects. To be able to for example reference the +# user through a ForeignKey, a User object must be materialized locally. We don't do +# that here, as this search might potentially return a lot of unrelated users since +# it's a wildcard match. +# Unlike the authentication, searching does not involve the browser - we just make +# a direct http call. +def user_search(searchterm=None, userid=None): + # If upsteam isn't responding quickly, it's not going to respond at all, and + # 10 seconds is already quite long. + socket.setdefaulttimeout(10) + if userid: + q = {'u': userid} + else: + q = {'s': searchterm} + + u = urllib.urlopen('%ssearch/?%s' % ( + settings.PGAUTH_REDIRECT, + urllib.urlencode(q), + )) + (ivs, datas) = u.read().split('&') + u.close() + + # Decryption time + decryptor = AES.new(base64.b64decode(settings.PGAUTH_KEY), + AES.MODE_CBC, + base64.b64decode(ivs, "-_")) + s = decryptor.decrypt(base64.b64decode(datas, "-_")).rstrip(' ') + j = simplejson.loads(s) + + return j -- 2.39.5