# HG changeset patch # User Brian Neal # Date 1416366564 21600 # Node ID 8743c566f712ac46d97095b2b581550a842b551c # Parent 1583a41511cc54eae1fbc5231cd72172ed879c21 WIP commit for converting to Google Calendar v3 API. This code should be enough to receive tokens from Google. See issue #80. diff -r 1583a41511cc -r 8743c566f712 gcalendar/admin.py --- a/gcalendar/admin.py Tue Nov 11 18:27:05 2014 -0600 +++ b/gcalendar/admin.py Tue Nov 18 21:09:24 2014 -0600 @@ -11,8 +11,6 @@ from django.http import HttpResponseRedirect from django.shortcuts import render -import gdata.client - from gcalendar.models import Event, AccessToken from gcalendar.calendar import Calendar, CalendarError from gcalendar import oauth @@ -20,9 +18,6 @@ import bio.badges -SCOPES = ['https://www.google.com/calendar/feeds/'] - - class EventAdmin(admin.ModelAdmin): list_display = ('what', 'user', 'start_date', 'where', 'date_submitted', 'status', 'is_approved', 'google_html') @@ -46,12 +41,12 @@ url(r'^google_sync/$', self.admin_site.admin_view(self.google_sync), name="gcalendar-google_sync"), - url(r'^fetch_auth/$', - self.admin_site.admin_view(self.fetch_auth), - name="gcalendar-fetch_auth"), - url(r'^get_access_token/$', - self.admin_site.admin_view(self.get_access_token), - name="gcalendar-get_access_token"), + url(r'^authorize/$', + self.admin_site.admin_view(self.authorize), + name="gcalendar-authorize"), + url(r'^auth_return/$', + self.admin_site.admin_view(self.auth_return), + name="gcalendar-auth_return"), ) return my_urls + urls @@ -84,10 +79,10 @@ # Get pending events events = Event.pending_events.all() - # Attempt to get saved access token to the Google calendar - access_token = AccessToken.objects.get_token().access_token() + # Check status of credentials file + cred_status = oauth.check_credentials_status() - messages = [] + msgs = [] err_msg = '' if request.method == 'POST': if access_token: @@ -100,50 +95,37 @@ err_msg = e.msg events = Event.pending_events.all() else: - messages.append('All events processed successfully.') + msgs.append('All events processed successfully.') events = Event.objects.none() return render(request, 'gcalendar/google_sync.html', { 'current_app': self.admin_site.name, - 'access_token': access_token, - 'messages': messages, + 'cred_status': cred_status, + 'messages': msgs, 'err_msg': err_msg, 'events': events, }) - def fetch_auth(self, request): + def authorize(self, request): """ - This view fetches a request token and then redirects the user to - authorize it. - + This view generates the authorization URL and redirects the user to it. """ site = Site.objects.get_current() callback_url = 'http://%s%s' % (site.domain, - reverse('admin:gcalendar-get_access_token')) - try: - auth_url = oauth.fetch_auth(request, SCOPES, callback_url) - except gdata.client.Error, e: - messages.error(request, str(e)) - return HttpResponseRedirect(reverse('admin:gcalendar-google_sync')) - else: - return HttpResponseRedirect(auth_url) + reverse('admin:gcalendar-auth_return')) + auth_url = oauth.get_auth_url(request, callback_url) + return HttpResponseRedirect(auth_url) - def get_access_token(self, request): + def auth_return(self, request): """ This view is called by Google after the user has authorized us access to - their data. We call into the oauth module to upgrade the oauth token to - an access token. We then save the access token in the database and - redirect back to our admin Google sync view. - + their data. We call into the oauth module to process the authorization + code and exchange it for tokens. """ try: - access_token = oauth.get_access_token(request) - except gdata.client.Error, e: - messages.error(request, str(e)) - else: - token = AccessToken.objects.get_token() - token.update(access_token) - token.save() + oauth.auth_return(request) + except oauth.OAuthError as e: + self.message_user(request, str(e), level=messages.ERROR) return HttpResponseRedirect(reverse('admin:gcalendar-google_sync')) diff -r 1583a41511cc -r 8743c566f712 gcalendar/oauth.py --- a/gcalendar/oauth.py Tue Nov 11 18:27:05 2014 -0600 +++ b/gcalendar/oauth.py Tue Nov 18 21:09:24 2014 -0600 @@ -2,84 +2,113 @@ This module handles the OAuth integration with Google. """ -from __future__ import with_statement +import datetime import logging +import os -import gdata.gauth -from gdata.calendar_resource.client import CalendarResourceClient - +from oauth2client.client import OAuth2WebServerFlow, FlowExchangeError +from oauth2client.file import Storage from django.conf import settings +from django.core.cache import cache logger = logging.getLogger(__name__) -USER_AGENT = 'surfguitar101-gcalendar-v1' -REQ_TOKEN_SESSION_KEY = 'gcalendar oauth request token' +FLOW_CACHE_KEY = 'gcalendar-oauth-flow' +FLOW_CACHE_TIMEOUT = 60 +SCOPE = 'https://www.googleapis.com/auth/calendar' -def fetch_auth(request, scopes, callback_url): +class OAuthError(Exception): + """Base exception for errors raised by the oauth module""" + + +def check_credentials_status(): + """Performs a stat() on the credentials file and returns the last modified + time as a datetime object. If an error occurs, None is returned. """ - This function fetches a request token from Google and stores it in the - session. It then returns the authorization URL as a string. + try: + status = os.stat(settings.GCAL_CREDENTIALS_PATH) + return datetime.datetime.fromtimestamp(status.st_mtime) + except OSError: + return None + + +def get_auth_url(request, callback_url): + """ + This function creates an OAuth flow object and uses it to generate an + authorization URL which is returned to the caller. request - the HttpRequest object for the user requesting the token. The token is stored in the session object attached to this request. - scopes - a list of scope strings that the request token is for. See - http://code.google.com/apis/gdata/faq.html#AuthScopes - callback_url - a string that is the URL that Google should redirect the user to after the user has authorized our application access to their data. - This function only supports RSA-SHA1 authentication. Settings in the Django - settings module determine the consumer key and path to the RSA private key. """ - logger.info("fetch_auth started; callback url='%s'", callback_url) - client = CalendarResourceClient(None, source=USER_AGENT) + logger.info("get_auth_url started; callback url='%s'", callback_url) - with open(settings.GOOGLE_OAUTH_PRIVATE_KEY_PATH, 'r') as f: - rsa_key = f.read() - logger.info("read RSA key; now getting request token") + flow = OAuth2WebServerFlow(client_id=settings.GCAL_CLIENT_ID, + client_secret=settings.GCAL_CLIENT_SECRET, + scope=SCOPE, + redirect_uri=callback_url) - request_token = client.GetOAuthToken( - scopes, - callback_url, - settings.GOOGLE_OAUTH_CONSUMER_KEY, - rsa_private_key=rsa_key) + auth_url = flow.step1_get_authorize_url() + logger.info("auth url: '%s'", auth_url) - logger.info("received token") - request.session[REQ_TOKEN_SESSION_KEY] = request_token + # Save the flow in the cache so we can use it when Google calls the + # callback. The expiration time also lets us check to make sure someone + # isn't spoofing google if we are called at some random time. + # Note: using the session might seem like a more obvious choice, but flow + # objects are not JSON-serializable, and we don't want to use pickelable + # sessions for security reasons. - auth_url = request_token.generate_authorization_url() - logger.info("generated auth url '%s'", str(auth_url)) + cache.set(FLOW_CACHE_KEY, flow, FLOW_CACHE_TIMEOUT) - return str(auth_url) + return auth_url -def get_access_token(request): +def auth_return(request): """ This function should be called after Google has sent the user back to us - after the user authorized us. We retrieve the oauth token from the request - URL and then upgrade it to an access token. We then return the access token. + after the user authorized us. We retrieve the authorization code from the + request query parameters and then exchange it for access tokens. These + tokens are stored in our credentials file. + If an error is encountered, an OAuthError is raised. """ - logger.info("get_access_token called as '%s'", request.get_full_path()) + logger.info("auth_return called as '%s'", request.get_full_path()) - saved_token = request.session.get(REQ_TOKEN_SESSION_KEY) - if saved_token is None: - logger.error("saved request token not found in session!") - return None + # Try to retrieve the flow object from the cache + flow = cache.get(FLOW_CACHE_KEY) + if not flow: + logger.warning("auth_return no flow in cache, bailing out") + raise OAuthError("No flow found. Perhaps we timed out?") - logger.info("extracting token...") - request_token = gdata.gauth.AuthorizeRequestToken(saved_token, - request.build_absolute_uri()) + # Delete flow out of cache to close our operational window + cache.delete(FLOW_CACHE_KEY) - logger.info("upgrading to access token...") + # Process the request + error = request.GET.get('error') + if error: + logging.error("auth_return received error: %s", error) + raise OAuthError("Error authorizing request: %s" % error) - client = CalendarResourceClient(None, source=USER_AGENT) - access_token = client.GetAccessToken(request_token) + code = request.GET.get('code') + if not code: + logging.error("auth_return missing code") + raise OAuthError("No authorization code received") - logger.info("upgraded to access token...") - return access_token + logging.info("auth_return exchanging code for credentials") + try: + credentials = flow.step2_exchange(code) + except FlowExchangeError as ex: + logging.error("auth_return exchange failed: %s", ex) + raise OAuthError(str(ex)) + + logging.info("auth_return storing credentials") + storage = Storage(settings.GCAL_CREDENTIALS_PATH) + storage.put(credentials) + logging.info("auth_return completed successfully") def serialize_token(token): diff -r 1583a41511cc -r 8743c566f712 sg101/settings/base.py --- a/sg101/settings/base.py Tue Nov 11 18:27:05 2014 -0600 +++ b/sg101/settings/base.py Tue Nov 18 21:09:24 2014 -0600 @@ -272,9 +272,10 @@ # GCalendar settings GCAL_CALENDAR_ID = 'i81lu3fkh57sgqqenogefd9v78@group.calendar.google.com' -# Google OAuth settings -GOOGLE_OAUTH_CONSUMER_KEY = 'surfguitar101.com' -GOOGLE_OAUTH_PRIVATE_KEY_PATH = SECRETS['GOOGLE_KEY_PATH'] +# GCalendar/Google OAuth settings +GCAL_CLIENT_ID = SECRETS['GCAL_CLIENT_ID'] +GCAL_CLIENT_SECRET = SECRETS['GCAL_CLIENT_SECRET'] +GCAL_CREDENTIALS_PATH = SECRETS['GCAL_CREDENTIALS_PATH'] # MoinMoin Wiki integration settings WIKI_COOKIE_NAME = 'sg101_wiki' diff -r 1583a41511cc -r 8743c566f712 sg101/templates/gcalendar/google_sync.html --- a/sg101/templates/gcalendar/google_sync.html Tue Nov 11 18:27:05 2014 -0600 +++ b/sg101/templates/gcalendar/google_sync.html Tue Nov 18 21:09:24 2014 -0600 @@ -16,7 +16,11 @@ {% endif %} -

Access token status: {% bool_icon access_token %} — Request new access token

+

Credentials file status: {% bool_icon cred_status %} +{% if cred_status %} +— Last modified: {{ cred_status|date:"r" }} +{% endif %} +— Request new credentials

{% if events %}

The following pending events have been approved and are ready to be synchronized with the Google calendar.