Commit 33ae93ec by Clinton Blackburn Committed by GitHub

Merge pull request #13321 from edx/clintonb/userinfo-cookie

Updated User Info Cookie
parents 7ba9048a 33636db9
...@@ -2,18 +2,43 @@ ...@@ -2,18 +2,43 @@
Utility functions for setting "logged in" cookies used by subdomains. Utility functions for setting "logged in" cookies used by subdomains.
""" """
import time
import json import json
import time
from django.dispatch import Signal import six
import urllib
from django.utils.http import cookie_date
from django.conf import settings from django.conf import settings
from django.core.urlresolvers import reverse, NoReverseMatch from django.core.urlresolvers import reverse, NoReverseMatch
from django.dispatch import Signal
from django.utils.http import cookie_date
from student.models import CourseEnrollment
CREATE_LOGON_COOKIE = Signal(providing_args=["user", "response"]) CREATE_LOGON_COOKIE = Signal(providing_args=["user", "response"])
def _get_cookie_settings(request):
""" Returns the common cookie settings (e.g. expiration time). """
if request.session.get_expire_at_browser_close():
max_age = None
expires = None
else:
max_age = request.session.get_expiry_age()
expires_time = time.time() + max_age
expires = cookie_date(expires_time)
cookie_settings = {
'max_age': max_age,
'expires': expires,
'domain': settings.SESSION_COOKIE_DOMAIN,
'path': '/',
'httponly': None,
}
return cookie_settings
def set_logged_in_cookies(request, response, user): def set_logged_in_cookies(request, response, user):
""" """
Set cookies indicating that the user is logged in. Set cookies indicating that the user is logged in.
...@@ -49,21 +74,7 @@ def set_logged_in_cookies(request, response, user): ...@@ -49,21 +74,7 @@ def set_logged_in_cookies(request, response, user):
HttpResponse HttpResponse
""" """
if request.session.get_expire_at_browser_close(): cookie_settings = _get_cookie_settings(request)
max_age = None
expires = None
else:
max_age = request.session.get_expiry_age()
expires_time = time.time() + max_age
expires = cookie_date(expires_time)
cookie_settings = {
'max_age': max_age,
'expires': expires,
'domain': settings.SESSION_COOKIE_DOMAIN,
'path': '/',
'httponly': None,
}
# Backwards compatibility: set the cookie indicating that the user # Backwards compatibility: set the cookie indicating that the user
# is logged in. This is just a boolean value, so it's not very useful. # is logged in. This is just a boolean value, so it's not very useful.
...@@ -76,6 +87,41 @@ def set_logged_in_cookies(request, response, user): ...@@ -76,6 +87,41 @@ def set_logged_in_cookies(request, response, user):
**cookie_settings **cookie_settings
) )
set_user_info_cookie(response, request, user)
# give signal receivers a chance to add cookies
CREATE_LOGON_COOKIE.send(sender=None, user=user, response=response)
return response
def set_user_info_cookie(response, request, user):
""" Sets the user info cookie on the response. """
cookie_settings = _get_cookie_settings(request)
# In production, TLS should be enabled so that this cookie is encrypted
# when we send it. We also need to set "secure" to True so that the browser
# will transmit it only over secure connections.
#
# In non-production environments (acceptance tests, devstack, and sandboxes),
# we still want to set this cookie. However, we do NOT want to set it to "secure"
# because the browser won't send it back to us. This can cause an infinite redirect
# loop in the third-party auth flow, which calls `is_logged_in_cookie_set` to determine
# whether it needs to set the cookie or continue to the next pipeline stage.
user_info_cookie_is_secure = request.is_secure()
user_info = get_user_info_cookie_data(request, user)
response.set_cookie(
settings.EDXMKTG_USER_INFO_COOKIE_NAME.encode('utf-8'),
urllib.quote(json.dumps(user_info)),
secure=user_info_cookie_is_secure,
**cookie_settings
)
def get_user_info_cookie_data(request, user):
""" Returns information that wil populate the user info cookie. """
# Set a cookie with user info. This can be used by external sites # Set a cookie with user info. This can be used by external sites
# to customize content based on user information. Currently, # to customize content based on user information. Currently,
# we include information that's used to customize the "account" # we include information that's used to customize the "account"
...@@ -94,38 +140,24 @@ def set_logged_in_cookies(request, response, user): ...@@ -94,38 +140,24 @@ def set_logged_in_cookies(request, response, user):
pass pass
# Convert relative URL paths to absolute URIs # Convert relative URL paths to absolute URIs
for url_name, url_path in header_urls.iteritems(): for url_name, url_path in six.iteritems(header_urls):
header_urls[url_name] = request.build_absolute_uri(url_path) header_urls[url_name] = request.build_absolute_uri(url_path)
enrollments = []
for enrollment in CourseEnrollment.enrollments_for_user(user):
enrollments.append({
'course_run_id': six.text_type(enrollment.course_id),
'seat_type': enrollment.mode
})
user_info = { user_info = {
'version': settings.EDXMKTG_USER_INFO_COOKIE_VERSION, 'version': settings.EDXMKTG_USER_INFO_COOKIE_VERSION,
'username': user.username, 'username': user.username,
'email': user.email,
'header_urls': header_urls, 'header_urls': header_urls,
'enrollments': enrollments,
} }
# In production, TLS should be enabled so that this cookie is encrypted return user_info
# when we send it. We also need to set "secure" to True so that the browser
# will transmit it only over secure connections.
#
# In non-production environments (acceptance tests, devstack, and sandboxes),
# we still want to set this cookie. However, we do NOT want to set it to "secure"
# because the browser won't send it back to us. This can cause an infinite redirect
# loop in the third-party auth flow, which calls `is_logged_in_cookie_set` to determine
# whether it needs to set the cookie or continue to the next pipeline stage.
user_info_cookie_is_secure = request.is_secure()
response.set_cookie(
settings.EDXMKTG_USER_INFO_COOKIE_NAME.encode('utf-8'),
json.dumps(user_info),
secure=user_info_cookie_is_secure,
**cookie_settings
)
# give signal receivers a chance to add cookies
CREATE_LOGON_COOKIE.send(sender=None, user=user, response=response)
return response
def delete_logged_in_cookies(response): def delete_logged_in_cookies(response):
......
"""Provides factories for student models.""" """Provides factories for student models."""
import random import random
from student.models import (User, UserProfile, Registration,
CourseEnrollmentAllowed, CourseEnrollment,
PendingEmailChange, UserStanding,
CourseAccessRole)
from course_modes.models import CourseMode
from django.contrib.auth.models import Group, AnonymousUser
from datetime import datetime from datetime import datetime
from uuid import uuid4
import factory import factory
from django.contrib.auth.models import Group, AnonymousUser
from factory import lazy_attribute from factory import lazy_attribute
from factory.django import DjangoModelFactory from factory.django import DjangoModelFactory
from uuid import uuid4
from pytz import UTC
from opaque_keys.edx.locations import SlashSeparatedCourseKey from opaque_keys.edx.locations import SlashSeparatedCourseKey
from pytz import UTC
from course_modes.models import CourseMode
from student.models import (
User, UserProfile, Registration, CourseEnrollmentAllowed, CourseEnrollment, PendingEmailChange, UserStanding,
CourseAccessRole
)
# Factories are self documenting # Factories are self documenting
# pylint: disable=missing-docstring # pylint: disable=missing-docstring
USER_PASSWORD = 'test'
class GroupFactory(DjangoModelFactory): class GroupFactory(DjangoModelFactory):
class Meta(object): class Meta(object):
model = Group model = Group
django_get_or_create = ('name', ) django_get_or_create = ('name',)
name = factory.Sequence(u'group{0}'.format) name = factory.Sequence(u'group{0}'.format)
...@@ -39,7 +42,7 @@ class UserStandingFactory(DjangoModelFactory): ...@@ -39,7 +42,7 @@ class UserStandingFactory(DjangoModelFactory):
class UserProfileFactory(DjangoModelFactory): class UserProfileFactory(DjangoModelFactory):
class Meta(object): class Meta(object):
model = UserProfile model = UserProfile
django_get_or_create = ('user', ) django_get_or_create = ('user',)
user = None user = None
name = factory.LazyAttribute(u'{0.user.first_name} {0.user.last_name}'.format) name = factory.LazyAttribute(u'{0.user.first_name} {0.user.last_name}'.format)
...@@ -83,7 +86,7 @@ class UserFactory(DjangoModelFactory): ...@@ -83,7 +86,7 @@ class UserFactory(DjangoModelFactory):
username = factory.Sequence(u'robot{0}'.format) username = factory.Sequence(u'robot{0}'.format)
email = factory.Sequence(u'robot+test+{0}@edx.org'.format) email = factory.Sequence(u'robot+test+{0}@edx.org'.format)
password = factory.PostGenerationMethodCall('set_password', 'test') password = factory.PostGenerationMethodCall('set_password', USER_PASSWORD)
first_name = factory.Sequence(u'Robot{0}'.format) first_name = factory.Sequence(u'Robot{0}'.format)
last_name = 'Test' last_name = 'Test'
is_staff = False is_staff = False
...@@ -155,6 +158,7 @@ class PendingEmailChangeFactory(DjangoModelFactory): ...@@ -155,6 +158,7 @@ class PendingEmailChangeFactory(DjangoModelFactory):
new_email: sequence of new+email+{}@edx.org new_email: sequence of new+email+{}@edx.org
activation_key: sequence of integers, padded to 30 characters activation_key: sequence of integers, padded to 30 characters
""" """
class Meta(object): class Meta(object):
model = PendingEmailChange model = PendingEmailChange
......
# pylint: disable=missing-docstring
from __future__ import unicode_literals
import six
from django.conf import settings
from django.core.urlresolvers import reverse
from django.test import RequestFactory
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import CourseFactory
from student.cookies import get_user_info_cookie_data
from student.tests.factories import UserFactory, CourseEnrollmentFactory
class CookieTests(SharedModuleStoreTestCase):
@classmethod
def setUpClass(cls):
super(CookieTests, cls).setUpClass()
cls.course = CourseFactory()
def setUp(self):
super(CookieTests, self).setUp()
self.user = UserFactory.create()
def _get_expected_header_urls(self, request):
expected_header_urls = {
'logout': reverse('logout'),
}
# Studio (CMS) does not have the URLs below
if settings.ROOT_URLCONF == 'lms.urls':
expected_header_urls.update({
'account_settings': reverse('account_settings'),
'learner_profile': reverse('learner_profile', kwargs={'username': self.user.username}),
})
# Convert relative URL paths to absolute URIs
for url_name, url_path in six.iteritems(expected_header_urls):
expected_header_urls[url_name] = request.build_absolute_uri(url_path)
return expected_header_urls
def test_get_user_info_cookie_data(self):
""" Verify the function returns data that """
request = RequestFactory().get('/')
request.user = self.user
enrollment_mode = 'verified'
course_id = self.course.id # pylint: disable=no-member
CourseEnrollmentFactory.create(user=self.user, course_id=course_id, mode=enrollment_mode)
actual = get_user_info_cookie_data(request, self.user)
expected_enrollments = [{
'course_run_id': six.text_type(course_id),
'seat_type': enrollment_mode,
}]
expected = {
'version': settings.EDXMKTG_USER_INFO_COOKIE_VERSION,
'username': self.user.username,
'header_urls': self._get_expected_header_urls(request),
'enrollments': expected_enrollments,
}
self.assertDictEqual(actual, expected)
...@@ -4,6 +4,7 @@ Tests for student activation and login ...@@ -4,6 +4,7 @@ Tests for student activation and login
import json import json
import unittest import unittest
import urllib
from django.test import TestCase from django.test import TestCase
from django.test.client import Client from django.test.client import Client
from django.test.utils import override_settings from django.test.utils import override_settings
...@@ -169,14 +170,10 @@ class LoginTest(CacheIsolationTestCase): ...@@ -169,14 +170,10 @@ class LoginTest(CacheIsolationTestCase):
# Verify the format of the "user info" cookie set on login # Verify the format of the "user info" cookie set on login
cookie = self.client.cookies[settings.EDXMKTG_USER_INFO_COOKIE_NAME] cookie = self.client.cookies[settings.EDXMKTG_USER_INFO_COOKIE_NAME]
user_info = json.loads(cookie.value) user_info = json.loads(urllib.unquote(cookie.value))
# Check that the version is set
self.assertEqual(user_info["version"], settings.EDXMKTG_USER_INFO_COOKIE_VERSION) self.assertEqual(user_info["version"], settings.EDXMKTG_USER_INFO_COOKIE_VERSION)
# Check that the username and email are set
self.assertEqual(user_info["username"], self.user.username) self.assertEqual(user_info["username"], self.user.username)
self.assertEqual(user_info["email"], self.user.email)
# Check that the URLs are absolute # Check that the URLs are absolute
for url in user_info["header_urls"].values(): for url in user_info["header_urls"].values():
......
...@@ -106,7 +106,7 @@ from student.helpers import ( ...@@ -106,7 +106,7 @@ from student.helpers import (
auth_pipeline_urls, get_next_url_for_login_page, auth_pipeline_urls, get_next_url_for_login_page,
DISABLE_UNENROLL_CERT_STATES, DISABLE_UNENROLL_CERT_STATES,
) )
from student.cookies import set_logged_in_cookies, delete_logged_in_cookies from student.cookies import set_logged_in_cookies, delete_logged_in_cookies, set_user_info_cookie
from student.models import anonymous_id_for_user, UserAttribute, EnrollStatusChange from student.models import anonymous_id_for_user, UserAttribute, EnrollStatusChange
from shoppingcart.models import DonationConfiguration, CourseRegistrationCode from shoppingcart.models import DonationConfiguration, CourseRegistrationCode
...@@ -749,7 +749,9 @@ def dashboard(request): ...@@ -749,7 +749,9 @@ def dashboard(request):
'ecommerce_payment_page': ecommerce_service.payment_page_url(), 'ecommerce_payment_page': ecommerce_service.payment_page_url(),
}) })
return render_to_response('dashboard.html', context) response = render_to_response('dashboard.html', context)
set_user_info_cookie(response, request, user)
return response
def _create_recent_enrollment_message(course_enrollments, course_modes): # pylint: disable=invalid-name def _create_recent_enrollment_message(course_enrollments, course_modes): # pylint: disable=invalid-name
......
...@@ -94,6 +94,7 @@ requests-oauthlib==0.4.1 ...@@ -94,6 +94,7 @@ requests-oauthlib==0.4.1
scipy==0.14.0 scipy==0.14.0
Shapely==1.2.16 Shapely==1.2.16
singledispatch==3.4.0.2 singledispatch==3.4.0.2
six>=1.10.0,<2.0.0
sorl-thumbnail==12.3 sorl-thumbnail==12.3
sortedcontainers==0.9.2 sortedcontainers==0.9.2
stevedore==1.10.0 stevedore==1.10.0
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment