diff --git a/cms/djangoapps/contentstore/rules.py b/cms/djangoapps/contentstore/rules.py
new file mode 100644
index 0000000000000000000000000000000000000000..14cb77bc12ec70be8fbf71049c1c2153c57a1a94
--- /dev/null
+++ b/cms/djangoapps/contentstore/rules.py
@@ -0,0 +1,9 @@
+"""
+Authorization rules related to content management.
+"""
+
+from __future__ import absolute_import, unicode_literals
+
+import user_tasks.rules
+
+user_tasks.rules.add_rules()
diff --git a/cms/envs/common.py b/cms/envs/common.py
index 07e17c727cabfa9bd14c96abe079da855561e767..6430250e70b27c4cf4c982aad2a31ca65698dd80 100644
--- a/cms/envs/common.py
+++ b/cms/envs/common.py
@@ -39,9 +39,12 @@ When refering to XBlocks, we use the entry-point name. For example,
 # want to import all variables from base settings files
 # pylint: disable=unused-import
 
+from __future__ import absolute_import
+
 import imp
 import os
 import sys
+from datetime import timedelta
 import lms.envs.common
 # Although this module itself may not use these imported variables, other dependent modules may.
 from lms.envs.common import (
@@ -300,6 +303,7 @@ LOGIN_URL = EDX_ROOT_URL + '/signin'
 
 # use the ratelimit backend to prevent brute force attacks
 AUTHENTICATION_BACKENDS = (
+    'rules.permissions.ObjectPermissionBackend',
     'ratelimitbackend.backends.RateLimitModelBackend',
 )
 
@@ -933,7 +937,13 @@ INSTALLED_APPS = (
     'django_sites_extensions',
 
     # additional release utilities to ease automation
-    'release_util'
+    'release_util',
+
+    # rule-based authorization
+    'rules.apps.AutodiscoverRulesConfig',
+
+    # management of user-triggered async tasks (course import/export, etc.)
+    'user_tasks',
 )
 
 
@@ -1212,3 +1222,8 @@ OAUTH2_PROVIDER_APPLICATION_MODEL = 'oauth2_provider.Application'
 # Used with Email sending
 RETRY_ACTIVATION_EMAIL_MAX_ATTEMPTS = 5
 RETRY_ACTIVATION_EMAIL_TIMEOUT = 0.5
+
+############## DJANGO-USER-TASKS ##############
+
+# How long until database records about the outcome of a task and its artifacts get deleted?
+USER_TASKS_MAX_AGE = timedelta(days=7)
diff --git a/cms/tests/test_user_tasks.py b/cms/tests/test_user_tasks.py
new file mode 100644
index 0000000000000000000000000000000000000000..ed924d66fc717a2447f0a9be869bc190879ffe71
--- /dev/null
+++ b/cms/tests/test_user_tasks.py
@@ -0,0 +1,107 @@
+"""
+Unit tests for integration of the django-user-tasks app and its REST API.
+"""
+
+from __future__ import absolute_import, print_function, unicode_literals
+
+from uuid import uuid4
+
+from django.contrib.auth.models import User
+from django.core.urlresolvers import reverse
+from django.test import override_settings
+
+from rest_framework.test import APITestCase
+from user_tasks.models import UserTaskArtifact, UserTaskStatus
+from user_tasks.serializers import ArtifactSerializer, StatusSerializer
+
+
+# Helper functions for stuff that pylint complains about without disable comments
+
+def _context(response):
+    """
+    Get a context dictionary for a serializer appropriate for the given response.
+    """
+    return {'request': response.wsgi_request}  # pylint: disable=no-member
+
+
+def _data(response):
+    """
+    Get the serialized data dictionary from the given REST API test response.
+    """
+    return response.data  # pylint: disable=no-member
+
+
+@override_settings(BROKER_URL='memory://localhost/')
+class TestUserTasks(APITestCase):
+    """
+    Tests of the django-user-tasks REST API endpoints.
+
+    Detailed tests of the default authorization rules are in the django-user-tasks code.
+    These tests just verify that the API is exposed and functioning.
+    """
+
+    @classmethod
+    def setUpTestData(cls):
+        cls.user = User.objects.create_user('test_user', 'test@example.com', 'password')
+        cls.status = UserTaskStatus.objects.create(
+            user=cls.user, task_id=str(uuid4()), task_class='test_rest_api.sample_task', name='SampleTask 2',
+            total_steps=5)
+        cls.artifact = UserTaskArtifact.objects.create(status=cls.status, text='Lorem ipsum')
+
+    def setUp(self):
+        super(TestUserTasks, self).setUp()
+        self.status.refresh_from_db()
+        self.client.force_authenticate(self.user)  # pylint: disable=no-member
+
+    def test_artifact_detail(self):
+        """
+        Users should be able to access artifacts for tasks they triggered.
+        """
+        response = self.client.get(reverse('usertaskartifact-detail', args=[self.artifact.uuid]))
+        assert response.status_code == 200
+        serializer = ArtifactSerializer(self.artifact, context=_context(response))
+        assert _data(response) == serializer.data
+
+    def test_artifact_list(self):
+        """
+        Users should be able to access a list of their tasks' artifacts.
+        """
+        response = self.client.get(reverse('usertaskartifact-list'))
+        assert response.status_code == 200
+        serializer = ArtifactSerializer(self.artifact, context=_context(response))
+        assert _data(response)['results'] == [serializer.data]
+
+    def test_status_cancel(self):
+        """
+        Users should be able to cancel tasks they no longer wish to complete.
+        """
+        response = self.client.post(reverse('usertaskstatus-cancel', args=[self.status.uuid]))
+        assert response.status_code == 200
+        self.status.refresh_from_db()
+        assert self.status.state == UserTaskStatus.CANCELED
+
+    def test_status_delete(self):
+        """
+        Users should be able to delete their own task status records when they're done with them.
+        """
+        response = self.client.delete(reverse('usertaskstatus-detail', args=[self.status.uuid]))
+        assert response.status_code == 204
+        assert not UserTaskStatus.objects.filter(pk=self.status.id).exists()
+
+    def test_status_detail(self):
+        """
+        Users should be able to access status records for tasks they triggered.
+        """
+        response = self.client.get(reverse('usertaskstatus-detail', args=[self.status.uuid]))
+        assert response.status_code == 200
+        serializer = StatusSerializer(self.status, context=_context(response))
+        assert _data(response) == serializer.data
+
+    def test_status_list(self):
+        """
+        Users should be able to access a list of their tasks' status records.
+        """
+        response = self.client.get(reverse('usertaskstatus-list'))
+        assert response.status_code == 200
+        serializer = StatusSerializer([self.status], context=_context(response), many=True)
+        assert _data(response)['results'] == serializer.data
diff --git a/cms/urls.py b/cms/urls.py
index 8c88027e51ba1c9e733bb1ffe09cbdc082ce0706..083e70a378365d2a2e66a23f7fad694c784bb881 100644
--- a/cms/urls.py
+++ b/cms/urls.py
@@ -113,6 +113,7 @@ urlpatterns += patterns(
     url(r'^group_configurations/{}/(?P<group_configuration_id>\d+)(/)?(?P<group_id>\d+)?$'.format(
         settings.COURSE_KEY_PATTERN), 'group_configurations_detail_handler'),
     url(r'^api/val/v0/', include('edxval.urls')),
+    url(r'^api/tasks/v0/', include('user_tasks.urls')),
 )
 
 JS_INFO_DICT = {
diff --git a/requirements/edx/base.txt b/requirements/edx/base.txt
index f8bf2c195abdd55fa01cd099ea5322144f00b837..bda6af8ccc1399ad15ac1734bfe292804746e186 100644
--- a/requirements/edx/base.txt
+++ b/requirements/edx/base.txt
@@ -34,6 +34,7 @@ django-simple-history==1.6.3
 django-statici18n==1.1.5
 django-storages==1.4.1
 django-method-override==0.1.0
+django-user-tasks==0.1.1
 # We need a fix to DRF 3.2.x, for now use it from our own cherry-picked repo
 #djangorestframework>=3.1,<3.2
 git+https://github.com/edx/django-rest-framework.git@3c72cb5ee5baebc4328947371195eae2077197b0#egg=djangorestframework==3.2.3
@@ -94,6 +95,7 @@ pysrt==0.4.7
 PyYAML==3.10
 requests==2.9.1
 requests-oauthlib==0.4.1
+rules==1.1.1
 scipy==0.14.0
 Shapely==1.2.16
 singledispatch==3.4.0.2