Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
93625671
Commit
93625671
authored
Jun 26, 2015
by
David Ormsbee
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #8620 from mcgachey/lti-deployment
[LTI Provider] Added an authentication backend to log in LTI users
parents
42d8091b
18734cf0
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
173 additions
and
20 deletions
+173
-20
lms/djangoapps/lti_provider/tests/test_users.py
+95
-10
lms/djangoapps/lti_provider/users.py
+66
-9
lms/envs/aws.py
+5
-1
lms/envs/common.py
+6
-0
lms/envs/test.py
+1
-0
No files found.
lms/djangoapps/lti_provider/tests/test_users.py
View file @
93625671
...
@@ -5,6 +5,7 @@ Tests for the LTI user management functionality
...
@@ -5,6 +5,7 @@ Tests for the LTI user management functionality
import
string
import
string
from
django.contrib.auth.models
import
User
from
django.contrib.auth.models
import
User
from
django.core.exceptions
import
PermissionDenied
from
django.test
import
TestCase
from
django.test
import
TestCase
from
django.test.client
import
RequestFactory
from
django.test.client
import
RequestFactory
from
mock
import
patch
,
MagicMock
from
mock
import
patch
,
MagicMock
...
@@ -25,21 +26,37 @@ class UserManagementHelperTest(TestCase):
...
@@ -25,21 +26,37 @@ class UserManagementHelperTest(TestCase):
self
.
new_user
=
UserFactory
.
create
()
self
.
new_user
=
UserFactory
.
create
()
self
.
new_user
.
save
()
self
.
new_user
.
save
()
self
.
request
.
user
=
self
.
old_user
self
.
request
.
user
=
self
.
old_user
self
.
lti_consumer
=
LtiConsumer
(
consumer_name
=
'TestConsumer'
,
consumer_key
=
'TestKey'
,
consumer_secret
=
'TestSecret'
)
self
.
lti_consumer
.
save
()
self
.
lti_user
=
LtiUser
(
self
.
lti_user
=
LtiUser
(
lti_user_id
=
'lti_user_id'
,
lti_user_id
=
'lti_user_id'
,
edx_user
=
self
.
new_user
edx_user
=
self
.
new_user
)
)
@patch
(
'django.contrib.auth.authenticate'
,
return_value
=
None
)
def
test_permission_denied_for_unknown_user
(
self
,
_authenticate_mock
):
with
self
.
assertRaises
(
PermissionDenied
):
users
.
switch_user
(
self
.
request
,
self
.
lti_user
,
self
.
lti_consumer
)
@patch
(
'lti_provider.users.login'
)
@patch
(
'lti_provider.users.login'
)
def
test_new_user_logged_in_by_switch_user
(
self
,
login_mock
):
def
test_authenticate_called
(
self
,
_login_mock
):
with
patch
(
'lti_provider.users.User.objects.get'
,
return_value
=
self
.
new_user
):
with
patch
(
'lti_provider.users.authenticate'
,
return_value
=
self
.
new_user
)
as
authenticate
:
users
.
switch_user
(
self
.
request
,
self
.
lti_user
)
users
.
switch_user
(
self
.
request
,
self
.
lti_user
,
self
.
lti_consumer
)
login_mock
.
assert_called_with
(
self
.
request
,
self
.
new_user
)
authenticate
.
assert_called_with
(
username
=
self
.
new_user
.
username
,
lti_user_id
=
self
.
lti_user
.
lti_user_id
,
lti_consumer
=
self
.
lti_consumer
)
@patch
(
'lti_provider.users.login'
)
@patch
(
'lti_provider.users.login'
)
def
test_backend_set_in_switch_user
(
self
,
_login_mock
):
def
test_login_called
(
self
,
login_mock
):
users
.
switch_user
(
self
.
request
,
self
.
lti_user
)
with
patch
(
'lti_provider.users.authenticate'
,
return_value
=
self
.
new_user
):
self
.
assertIsNotNone
(
self
.
new_user
.
backend
,
'Backend not set on user'
)
users
.
switch_user
(
self
.
request
,
self
.
lti_user
,
self
.
lti_consumer
)
login_mock
.
assert_called_with
(
self
.
request
,
self
.
new_user
)
def
test_random_username_generator
(
self
):
def
test_random_username_generator
(
self
):
for
_idx
in
range
(
1000
):
for
_idx
in
range
(
1000
):
...
@@ -93,7 +110,7 @@ class AuthenticateLtiUserTest(TestCase):
...
@@ -93,7 +110,7 @@ class AuthenticateLtiUserTest(TestCase):
with
patch
(
'lti_provider.users.create_lti_user'
,
return_value
=
lti_user
)
as
create_user
:
with
patch
(
'lti_provider.users.create_lti_user'
,
return_value
=
lti_user
)
as
create_user
:
users
.
authenticate_lti_user
(
self
.
request
,
self
.
lti_user_id
,
self
.
lti_consumer
)
users
.
authenticate_lti_user
(
self
.
request
,
self
.
lti_user_id
,
self
.
lti_consumer
)
create_user
.
assert_called_with
(
self
.
lti_user_id
,
self
.
lti_consumer
)
create_user
.
assert_called_with
(
self
.
lti_user_id
,
self
.
lti_consumer
)
switch_user
.
assert_called_with
(
self
.
request
,
lti_user
)
switch_user
.
assert_called_with
(
self
.
request
,
lti_user
,
self
.
lti_consumer
)
def
test_authentication_with_authenticated_user
(
self
,
create_user
,
switch_user
):
def
test_authentication_with_authenticated_user
(
self
,
create_user
,
switch_user
):
lti_user
=
self
.
create_lti_user_model
()
lti_user
=
self
.
create_lti_user_model
()
...
@@ -109,7 +126,7 @@ class AuthenticateLtiUserTest(TestCase):
...
@@ -109,7 +126,7 @@ class AuthenticateLtiUserTest(TestCase):
self
.
request
.
user
.
is_authenticated
=
MagicMock
(
return_value
=
False
)
self
.
request
.
user
.
is_authenticated
=
MagicMock
(
return_value
=
False
)
users
.
authenticate_lti_user
(
self
.
request
,
self
.
lti_user_id
,
self
.
lti_consumer
)
users
.
authenticate_lti_user
(
self
.
request
,
self
.
lti_user_id
,
self
.
lti_consumer
)
self
.
assertFalse
(
create_user
.
called
)
self
.
assertFalse
(
create_user
.
called
)
switch_user
.
assert_called_with
(
self
.
request
,
lti_user
)
switch_user
.
assert_called_with
(
self
.
request
,
lti_user
,
self
.
lti_consumer
)
def
test_authentication_with_wrong_user
(
self
,
create_user
,
switch_user
):
def
test_authentication_with_wrong_user
(
self
,
create_user
,
switch_user
):
lti_user
=
self
.
create_lti_user_model
()
lti_user
=
self
.
create_lti_user_model
()
...
@@ -117,7 +134,7 @@ class AuthenticateLtiUserTest(TestCase):
...
@@ -117,7 +134,7 @@ class AuthenticateLtiUserTest(TestCase):
self
.
request
.
user
.
is_authenticated
=
MagicMock
(
return_value
=
True
)
self
.
request
.
user
.
is_authenticated
=
MagicMock
(
return_value
=
True
)
users
.
authenticate_lti_user
(
self
.
request
,
self
.
lti_user_id
,
self
.
lti_consumer
)
users
.
authenticate_lti_user
(
self
.
request
,
self
.
lti_user_id
,
self
.
lti_consumer
)
self
.
assertFalse
(
create_user
.
called
)
self
.
assertFalse
(
create_user
.
called
)
switch_user
.
assert_called_with
(
self
.
request
,
lti_user
)
switch_user
.
assert_called_with
(
self
.
request
,
lti_user
,
self
.
lti_consumer
)
class
CreateLtiUserTest
(
TestCase
):
class
CreateLtiUserTest
(
TestCase
):
...
@@ -155,3 +172,71 @@ class CreateLtiUserTest(TestCase):
...
@@ -155,3 +172,71 @@ class CreateLtiUserTest(TestCase):
self
.
assertEqual
(
User
.
objects
.
count
(),
2
)
self
.
assertEqual
(
User
.
objects
.
count
(),
2
)
user
=
User
.
objects
.
get
(
username
=
'new_edx_id'
)
user
=
User
.
objects
.
get
(
username
=
'new_edx_id'
)
self
.
assertEqual
(
user
.
email
,
'new_edx_id@lti.example.com'
)
self
.
assertEqual
(
user
.
email
,
'new_edx_id@lti.example.com'
)
class
LtiBackendTest
(
TestCase
):
"""
Tests for the authentication backend that authenticates LTI users.
"""
def
setUp
(
self
):
super
(
LtiBackendTest
,
self
)
.
setUp
()
self
.
edx_user
=
UserFactory
.
create
()
self
.
edx_user
.
save
()
self
.
lti_consumer
=
LtiConsumer
(
consumer_key
=
"Consumer Key"
,
consumer_secret
=
"Consumer Secret"
)
self
.
lti_consumer
.
save
()
self
.
lti_user_id
=
'LTI User ID'
LtiUser
(
lti_consumer
=
self
.
lti_consumer
,
lti_user_id
=
self
.
lti_user_id
,
edx_user
=
self
.
edx_user
)
.
save
()
def
test_valid_user_authenticates
(
self
):
user
=
users
.
LtiBackend
()
.
authenticate
(
username
=
self
.
edx_user
.
username
,
lti_user_id
=
self
.
lti_user_id
,
lti_consumer
=
self
.
lti_consumer
)
self
.
assertEqual
(
user
,
self
.
edx_user
)
def
test_missing_user_returns_none
(
self
):
user
=
users
.
LtiBackend
()
.
authenticate
(
username
=
self
.
edx_user
.
username
,
lti_user_id
=
'Invalid Username'
,
lti_consumer
=
self
.
lti_consumer
)
self
.
assertIsNone
(
user
)
def
test_non_lti_user_returns_none
(
self
):
non_edx_user
=
UserFactory
.
create
()
non_edx_user
.
save
()
user
=
users
.
LtiBackend
()
.
authenticate
(
username
=
non_edx_user
.
username
,
)
self
.
assertIsNone
(
user
)
def
test_missing_lti_id_returns_null
(
self
):
user
=
users
.
LtiBackend
()
.
authenticate
(
username
=
self
.
edx_user
.
username
,
lti_consumer
=
self
.
lti_consumer
)
self
.
assertIsNone
(
user
)
def
test_missing_lti_consumer_returns_null
(
self
):
user
=
users
.
LtiBackend
()
.
authenticate
(
username
=
self
.
edx_user
.
username
,
lti_user_id
=
self
.
lti_user_id
,
)
self
.
assertIsNone
(
user
)
def
test_existing_user_returned_by_get_user
(
self
):
user
=
users
.
LtiBackend
()
.
get_user
(
self
.
edx_user
.
id
)
self
.
assertEqual
(
user
,
self
.
edx_user
)
def
test_get_user_returns_none_for_invalid_user
(
self
):
user
=
users
.
LtiBackend
()
.
get_user
(
-
1
)
self
.
assertIsNone
(
user
)
lms/djangoapps/lti_provider/users.py
View file @
93625671
...
@@ -7,11 +7,13 @@ import string
...
@@ -7,11 +7,13 @@ import string
import
random
import
random
import
uuid
import
uuid
from
django.contrib.auth
import
login
from
django.conf
import
settings
from
django.contrib.auth
import
authenticate
,
login
from
django.contrib.auth.models
import
User
from
django.contrib.auth.models
import
User
from
django.core.exceptions
import
PermissionDenied
from
django.db
import
IntegrityError
from
django.db
import
IntegrityError
from
lti_provider.models
import
LtiUser
from
lti_provider.models
import
LtiUser
from
student.models
import
UserProfile
def
authenticate_lti_user
(
request
,
lti_user_id
,
lti_consumer
):
def
authenticate_lti_user
(
request
,
lti_user_id
,
lti_consumer
):
...
@@ -36,7 +38,7 @@ def authenticate_lti_user(request, lti_user_id, lti_consumer):
...
@@ -36,7 +38,7 @@ def authenticate_lti_user(request, lti_user_id, lti_consumer):
request
.
user
==
lti_user
.
edx_user
):
request
.
user
==
lti_user
.
edx_user
):
# The user is not authenticated, or is logged in as somebody else.
# The user is not authenticated, or is logged in as somebody else.
# Switch them to the LTI user
# Switch them to the LTI user
switch_user
(
request
,
lti_user
)
switch_user
(
request
,
lti_user
,
lti_consumer
)
def
create_lti_user
(
lti_user_id
,
lti_consumer
):
def
create_lti_user
(
lti_user_id
,
lti_consumer
):
...
@@ -50,12 +52,17 @@ def create_lti_user(lti_user_id, lti_consumer):
...
@@ -50,12 +52,17 @@ def create_lti_user(lti_user_id, lti_consumer):
while
not
created
:
while
not
created
:
try
:
try
:
edx_user_id
=
generate_random_edx_username
()
edx_user_id
=
generate_random_edx_username
()
edx_email
=
"{}@{}"
.
format
(
edx_user_id
,
settings
.
LTI_USER_EMAIL_DOMAIN
)
edx_user
=
User
.
objects
.
create_user
(
edx_user
=
User
.
objects
.
create_user
(
username
=
edx_user_id
,
username
=
edx_user_id
,
password
=
edx_password
,
password
=
edx_password
,
email
=
'{}@lti.example.com'
.
format
(
edx_user_id
)
email
=
edx_email
,
)
)
edx_user
.
save
()
# A profile is required if PREVENT_CONCURRENT_LOGINS flag is set.
# TODO: We could populate user information from the LTI launch here,
# but it's not necessary for our current uses.
edx_user_profile
=
UserProfile
(
user
=
edx_user
)
edx_user_profile
.
save
()
created
=
True
created
=
True
except
IntegrityError
:
except
IntegrityError
:
# The random edx_user_id wasn't unique. Since 'created' is still
# The random edx_user_id wasn't unique. Since 'created' is still
...
@@ -71,14 +78,21 @@ def create_lti_user(lti_user_id, lti_consumer):
...
@@ -71,14 +78,21 @@ def create_lti_user(lti_user_id, lti_consumer):
return
lti_user
return
lti_user
def
switch_user
(
request
,
lti_user
):
def
switch_user
(
request
,
lti_user
,
lti_consumer
):
"""
"""
Log out the current user, and log in using the edX identity associated with
Log out the current user, and log in using the edX identity associated with
the LTI ID.
the LTI ID.
"""
"""
# The login function wants to know what backend authenticated the user.
edx_user
=
authenticate
(
lti_user
.
edx_user
.
backend
=
'LTI_Provider'
username
=
lti_user
.
edx_user
.
username
,
login
(
request
,
lti_user
.
edx_user
)
lti_user_id
=
lti_user
.
lti_user_id
,
lti_consumer
=
lti_consumer
)
if
not
edx_user
:
# This shouldn't happen, since we've created edX accounts for any LTI
# users by this point, but just in case we can return a 403.
raise
PermissionDenied
()
login
(
request
,
edx_user
)
def
generate_random_edx_username
():
def
generate_random_edx_username
():
...
@@ -92,3 +106,46 @@ def generate_random_edx_username():
...
@@ -92,3 +106,46 @@ def generate_random_edx_username():
for
_index
in
range
(
30
):
for
_index
in
range
(
30
):
username
=
username
+
random
.
SystemRandom
()
.
choice
(
allowable_chars
)
username
=
username
+
random
.
SystemRandom
()
.
choice
(
allowable_chars
)
return
username
return
username
class
LtiBackend
(
object
):
"""
A Django authentication backend that authenticates users via LTI. This
backend will only return a User object if it is associated with an LTI
identity (i.e. the user was created by the create_lti_user method above).
"""
def
authenticate
(
self
,
username
=
None
,
lti_user_id
=
None
,
lti_consumer
=
None
):
"""
Try to authenticate a user. This method will return a Django user object
if a user with the corresponding username exists in the database, and
if a record that links that user with an LTI user_id field exists in
the LtiUser collection.
If such a user is not found, the method returns None (in line with the
authentication backend specification).
"""
try
:
edx_user
=
User
.
objects
.
get
(
username
=
username
)
except
User
.
DoesNotExist
:
return
None
try
:
LtiUser
.
objects
.
get
(
edx_user_id
=
edx_user
.
id
,
lti_user_id
=
lti_user_id
,
lti_consumer
=
lti_consumer
)
except
LtiUser
.
DoesNotExist
:
return
None
return
edx_user
def
get_user
(
self
,
user_id
):
"""
Return the User object for a user that has already been authenticated by
this backend.
"""
try
:
return
User
.
objects
.
get
(
id
=
user_id
)
except
User
.
DoesNotExist
:
return
None
lms/envs/aws.py
View file @
93625671
...
@@ -647,6 +647,10 @@ EDXNOTES_INTERNAL_API = ENV_TOKENS.get('EDXNOTES_INTERNAL_API', EDXNOTES_INTERNA
...
@@ -647,6 +647,10 @@ EDXNOTES_INTERNAL_API = ENV_TOKENS.get('EDXNOTES_INTERNAL_API', EDXNOTES_INTERNA
CREDIT_PROVIDER_SECRET_KEYS
=
AUTH_TOKENS
.
get
(
"CREDIT_PROVIDER_SECRET_KEYS"
,
{})
CREDIT_PROVIDER_SECRET_KEYS
=
AUTH_TOKENS
.
get
(
"CREDIT_PROVIDER_SECRET_KEYS"
,
{})
############ CERTIFICATE VERIFICATION URL (STATIC FILES) ###########
############ CERTIFICATE VERIFICATION URL (STATIC FILES) ###########
ENV_TOKENS
.
get
(
'CERTIFICATES_STATIC_VERIFY_URL'
,
CERTIFICATES_STATIC_VERIFY_URL
)
ENV_TOKENS
.
get
(
'CERTIFICATES_STATIC_VERIFY_URL'
,
CERTIFICATES_STATIC_VERIFY_URL
)
##################### LTI Provider #####################
if
FEATURES
.
get
(
'ENABLE_LTI_PROVIDER'
):
INSTALLED_APPS
+=
(
'lti_provider'
,)
AUTHENTICATION_BACKENDS
+=
(
'lti_provider.users.LtiBackend'
,
)
lms/envs/common.py
View file @
93625671
...
@@ -2549,3 +2549,9 @@ CREDIT_PROVIDER_SECRET_KEYS = {}
...
@@ -2549,3 +2549,9 @@ CREDIT_PROVIDER_SECRET_KEYS = {}
# when a credit provider notifies us that a student has been approved
# when a credit provider notifies us that a student has been approved
# or denied for credit.
# or denied for credit.
CREDIT_PROVIDER_TIMESTAMP_EXPIRATION
=
15
*
60
CREDIT_PROVIDER_TIMESTAMP_EXPIRATION
=
15
*
60
# Default domain for the e-mail address associated with users who are created
# via the LTI Provider feature. Note that the generated e-mail addresses are
# not expected to be active; this setting simply allows administrators to
# route any messages intended for LTI users to a common domain.
LTI_USER_EMAIL_DOMAIN
=
'lti.example.com'
lms/envs/test.py
View file @
93625671
...
@@ -495,3 +495,4 @@ PROFILE_IMAGE_MIN_BYTES = 100
...
@@ -495,3 +495,4 @@ PROFILE_IMAGE_MIN_BYTES = 100
# Enable the LTI provider feature for testing
# Enable the LTI provider feature for testing
FEATURES
[
'ENABLE_LTI_PROVIDER'
]
=
True
FEATURES
[
'ENABLE_LTI_PROVIDER'
]
=
True
INSTALLED_APPS
+=
(
'lti_provider'
,)
INSTALLED_APPS
+=
(
'lti_provider'
,)
AUTHENTICATION_BACKENDS
+=
(
'lti_provider.users.LtiBackend'
,)
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment