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
a8d1d663
Commit
a8d1d663
authored
Oct 12, 2015
by
Fred Smith
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #538 from edx-solutions/rc/2015-10-07
Rc/2015 10 07
parents
341fdf46
8e7a6def
Hide whitespace changes
Inline
Side-by-side
Showing
27 changed files
with
600 additions
and
38 deletions
+600
-38
cms/envs/common.py
+1
-0
common/djangoapps/third_party_auth/api/__init__.py
+0
-0
common/djangoapps/third_party_auth/api/tests/test_views.py
+132
-0
common/djangoapps/third_party_auth/api/urls.py
+12
-0
common/djangoapps/third_party_auth/api/views.py
+91
-0
common/djangoapps/third_party_auth/models.py
+12
-0
common/djangoapps/third_party_auth/pipeline.py
+51
-8
common/djangoapps/third_party_auth/settings.py
+3
-0
common/djangoapps/third_party_auth/strategy.py
+36
-0
common/djangoapps/third_party_auth/templates/third_party_auth/post_custom_auth_entry.html
+23
-0
common/djangoapps/third_party_auth/tests/specs/test_google.py
+97
-1
common/djangoapps/third_party_auth/tests/test_pipeline.py
+1
-1
common/djangoapps/third_party_auth/tests/test_strategy.py
+42
-1
common/djangoapps/third_party_auth/urls.py
+2
-1
common/djangoapps/third_party_auth/views.py
+24
-1
lms/djangoapps/api_manager/sessions/views.py
+36
-10
lms/djangoapps/api_manager/users/tests.py
+4
-2
lms/djangoapps/api_manager/users/urls.py
+0
-4
lms/djangoapps/api_manager/users/views.py
+3
-1
lms/djangoapps/gradebook/receivers.py
+1
-1
lms/envs/aws.py
+7
-0
lms/envs/common.py
+1
-0
lms/envs/test.py
+8
-0
lms/urls.py
+1
-0
openedx/core/djangoapps/credit/signals.py
+1
-1
requirements/edx/custom.txt
+9
-5
requirements/edx/github.txt
+2
-1
No files found.
cms/envs/common.py
View file @
a8d1d663
...
@@ -1073,6 +1073,7 @@ NOTIFICATION_CLICK_LINK_URL_MAPS = {
...
@@ -1073,6 +1073,7 @@ NOTIFICATION_CLICK_LINK_URL_MAPS = {
'open-edx.lms.leaderboard.*'
:
'/courses/{course_id}/cohort'
,
'open-edx.lms.leaderboard.*'
:
'/courses/{course_id}/cohort'
,
'open-edx.lms.discussions.*'
:
'/courses/{course_id}/discussion/{commentable_id}/threads/{thread_id}'
,
'open-edx.lms.discussions.*'
:
'/courses/{course_id}/discussion/{commentable_id}/threads/{thread_id}'
,
'open-edx.xblock.group-project.*'
:
'/courses/{course_id}/group_work?seqid={activity_location}'
,
'open-edx.xblock.group-project.*'
:
'/courses/{course_id}/group_work?seqid={activity_location}'
,
'open-edx.xblock.group-project-v2.*'
:
'/courses/{course_id}/group_work?activate_block_id={location}'
,
}
}
# list all known channel providers
# list all known channel providers
...
...
common/djangoapps/third_party_auth/api/__init__.py
0 → 100644
View file @
a8d1d663
common/djangoapps/third_party_auth/api/tests/test_views.py
0 → 100644
View file @
a8d1d663
"""
Tests for the Third Party Auth REST API
"""
import
json
import
unittest
import
ddt
from
mock
import
patch
from
django.test
import
Client
from
django.core.urlresolvers
import
reverse
from
rest_framework.test
import
APITestCase
from
rest_framework
import
status
from
django.conf
import
settings
from
django.test.utils
import
override_settings
from
util.testing
import
UrlResetMixin
from
openedx.core.lib.django_test_client_utils
import
get_absolute_url
from
social.apps.django_app.default.models
import
UserSocialAuth
from
student.tests.factories
import
UserFactory
from
third_party_auth.tests.testutil
import
ThirdPartyAuthTestMixin
VALID_API_KEY
=
"i am a key"
@override_settings
(
EDX_API_KEY
=
VALID_API_KEY
)
@ddt.ddt
@unittest.skipUnless
(
settings
.
ROOT_URLCONF
==
'lms.urls'
,
'Test only valid in lms'
)
class
ThirdPartyAuthAPITests
(
ThirdPartyAuthTestMixin
,
APITestCase
):
"""
Test the Third Party Auth REST API
"""
ALICE_USERNAME
=
"alice"
CARL_USERNAME
=
"carl"
STAFF_USERNAME
=
"staff"
ADMIN_USERNAME
=
"admin"
# These users will be created and linked to third party accounts:
LINKED_USERS
=
(
ALICE_USERNAME
,
STAFF_USERNAME
,
ADMIN_USERNAME
)
PASSWORD
=
"edx"
def
setUp
(
self
):
""" Create users for use in the tests """
super
(
ThirdPartyAuthAPITests
,
self
)
.
setUp
()
google
=
self
.
configure_google_provider
(
enabled
=
True
)
self
.
configure_facebook_provider
(
enabled
=
True
)
self
.
configure_linkedin_provider
(
enabled
=
False
)
self
.
enable_saml
()
testshib
=
self
.
configure_saml_provider
(
name
=
'TestShib'
,
enabled
=
True
,
idp_slug
=
'testshib'
)
# Create several users and link each user to Google and TestShib
for
username
in
self
.
LINKED_USERS
:
make_superuser
=
(
username
==
self
.
ADMIN_USERNAME
)
make_staff
=
(
username
==
self
.
STAFF_USERNAME
)
or
make_superuser
user
=
UserFactory
.
create
(
username
=
username
,
password
=
self
.
PASSWORD
,
is_staff
=
make_staff
,
is_superuser
=
make_superuser
)
UserSocialAuth
.
objects
.
create
(
user
=
user
,
provider
=
google
.
backend_name
,
uid
=
'{}@gmail.com'
.
format
(
username
),
)
UserSocialAuth
.
objects
.
create
(
user
=
user
,
provider
=
testshib
.
backend_name
,
uid
=
'{}:{}'
.
format
(
testshib
.
idp_slug
,
username
),
)
# Create another user not linked to any providers:
UserFactory
.
create
(
username
=
self
.
CARL_USERNAME
,
password
=
self
.
PASSWORD
)
def
expected_active
(
self
,
username
):
""" The JSON active providers list response expected for the given user """
if
username
not
in
self
.
LINKED_USERS
:
return
[]
return
[
{
"provider_id"
:
"oa2-google-oauth2"
,
"name"
:
"Google"
,
"remote_id"
:
"{}@gmail.com"
.
format
(
username
),
},
{
"provider_id"
:
"saml-testshib"
,
"name"
:
"TestShib"
,
# The "testshib:" prefix is stored in the UserSocialAuth.uid field but should
# not be present in the 'remote_id', since that's an implementation detail:
"remote_id"
:
username
,
},
]
@ddt.data
(
# Any user can query their own list of providers
(
ALICE_USERNAME
,
ALICE_USERNAME
,
200
),
(
CARL_USERNAME
,
CARL_USERNAME
,
200
),
# A regular user cannot query another user nor deduce the existence of users based on the status code
(
ALICE_USERNAME
,
STAFF_USERNAME
,
403
),
(
ALICE_USERNAME
,
"nonexistent_user"
,
403
),
# Even Staff cannot query other users
(
STAFF_USERNAME
,
ALICE_USERNAME
,
403
),
# But admins can
(
ADMIN_USERNAME
,
ALICE_USERNAME
,
200
),
(
ADMIN_USERNAME
,
CARL_USERNAME
,
200
),
(
ADMIN_USERNAME
,
"invalid_username"
,
404
),
)
@ddt.unpack
def
test_list_connected_providers
(
self
,
request_user
,
target_user
,
expect_result
):
self
.
client
.
login
(
username
=
request_user
,
password
=
self
.
PASSWORD
)
url
=
reverse
(
'third_party_auth_users_api'
,
kwargs
=
{
'username'
:
target_user
})
response
=
self
.
client
.
get
(
url
)
self
.
assertEqual
(
response
.
status_code
,
expect_result
)
if
expect_result
==
200
:
self
.
assertIn
(
"active"
,
response
.
data
)
self
.
assertItemsEqual
(
response
.
data
[
"active"
],
self
.
expected_active
(
target_user
))
@ddt.data
(
# A server with a valid API key can query any user's list of providers
(
VALID_API_KEY
,
ALICE_USERNAME
,
200
),
(
VALID_API_KEY
,
"invalid_username"
,
404
),
(
"i am an invalid key"
,
ALICE_USERNAME
,
403
),
(
None
,
ALICE_USERNAME
,
403
),
)
@ddt.unpack
def
test_list_connected_providers__withapi_key
(
self
,
api_key
,
target_user
,
expect_result
):
url
=
reverse
(
'third_party_auth_users_api'
,
kwargs
=
{
'username'
:
target_user
})
response
=
self
.
client
.
get
(
url
,
HTTP_X_EDX_API_KEY
=
api_key
)
self
.
assertEqual
(
response
.
status_code
,
expect_result
)
if
expect_result
==
200
:
self
.
assertIn
(
"active"
,
response
.
data
)
self
.
assertItemsEqual
(
response
.
data
[
"active"
],
self
.
expected_active
(
target_user
))
common/djangoapps/third_party_auth/api/urls.py
0 → 100644
View file @
a8d1d663
""" URL configuration for the third party auth API """
from
django.conf.urls
import
patterns
,
url
from
.views
import
UserView
USERNAME_PATTERN
=
r'(?P<username>[\w.+-]+)'
urlpatterns
=
patterns
(
''
,
url
(
r'^v0/users/'
+
USERNAME_PATTERN
+
'$'
,
UserView
.
as_view
(),
name
=
'third_party_auth_users_api'
),
)
common/djangoapps/third_party_auth/api/views.py
0 → 100644
View file @
a8d1d663
"""
Third Party Auth REST API views
"""
from
django.contrib.auth.models
import
User
from
openedx.core.lib.api.authentication
import
(
OAuth2AuthenticationAllowInactiveUser
,
SessionAuthenticationAllowInactiveUser
,
)
from
openedx.core.lib.api.permissions
import
(
ApiKeyHeaderPermission
,
)
from
rest_framework
import
status
from
rest_framework.response
import
Response
from
rest_framework.views
import
APIView
from
third_party_auth
import
pipeline
class
UserView
(
APIView
):
"""
List the third party auth accounts linked to the specified user account.
**Example Request**
GET /api/third_party_auth/v0/users/{username}
**Response Values**
If the request for information about the user is successful, an HTTP 200 "OK" response
is returned.
The HTTP 200 response has the following values.
* active: A list of all the third party auth providers currently linked
to the given user's account. Each object in this list has the
following attributes:
* provider_id: The unique identifier of this provider (string)
* name: The name of this provider (string)
* remote_id: The ID of the user according to the provider. This ID
is what is used to link the user to their edX account during
login.
"""
authentication_classes
=
(
# Users may want to view/edit the providers used for authentication before they've
# activated their account, so we allow inactive users.
OAuth2AuthenticationAllowInactiveUser
,
SessionAuthenticationAllowInactiveUser
,
)
def
get
(
self
,
request
,
username
):
"""Create, read, or update enrollment information for a user.
HTTP Endpoint for all CRUD operations for a user course enrollment. Allows creation, reading, and
updates of the current enrollment for a particular course.
Args:
request (Request): The HTTP GET request
username (str): Fetch the list of providers linked to this user
Return:
JSON serialized list of the providers linked to this user.
"""
if
request
.
user
.
username
!=
username
:
# We are querying permissions for a user other than the current user.
if
not
request
.
user
.
is_superuser
and
not
ApiKeyHeaderPermission
()
.
has_permission
(
request
,
self
):
# Return a 403 (Unauthorized) without validating 'username', so that we
# do not let users probe the existence of other user accounts.
return
Response
(
status
=
status
.
HTTP_403_FORBIDDEN
)
try
:
user
=
User
.
objects
.
get
(
username
=
username
)
except
User
.
DoesNotExist
:
return
Response
(
status
=
status
.
HTTP_404_NOT_FOUND
)
providers
=
pipeline
.
get_provider_user_states
(
user
)
active_providers
=
[
{
"provider_id"
:
assoc
.
provider
.
provider_id
,
"name"
:
assoc
.
provider
.
name
,
"remote_id"
:
assoc
.
remote_id
,
}
for
assoc
in
providers
if
assoc
.
has_account
]
# In the future this can be trivially modified to return the inactive/disconnected providers as well.
return
Response
({
"active"
:
active_providers
})
common/djangoapps/third_party_auth/models.py
View file @
a8d1d663
...
@@ -133,6 +133,12 @@ class ProviderConfig(ConfigurationModel):
...
@@ -133,6 +133,12 @@ class ProviderConfig(ConfigurationModel):
""" Is this provider being used for this UserSocialAuth entry? """
""" Is this provider being used for this UserSocialAuth entry? """
return
self
.
backend_name
==
social_auth
.
provider
return
self
.
backend_name
==
social_auth
.
provider
def
get_remote_id_from_social_auth
(
self
,
social_auth
):
""" Given a UserSocialAuth object, return the remote ID used by this provider. """
# This is generally the same thing as the UID, expect when one backend is used for multiple providers
assert
self
.
match_social_auth
(
social_auth
)
return
social_auth
.
uid
@classmethod
@classmethod
def
get_register_form_data
(
cls
,
pipeline_kwargs
):
def
get_register_form_data
(
cls
,
pipeline_kwargs
):
"""Gets dict of data to display on the register form.
"""Gets dict of data to display on the register form.
...
@@ -281,6 +287,12 @@ class SAMLProviderConfig(ProviderConfig):
...
@@ -281,6 +287,12 @@ class SAMLProviderConfig(ProviderConfig):
prefix
=
self
.
idp_slug
+
":"
prefix
=
self
.
idp_slug
+
":"
return
self
.
backend_name
==
social_auth
.
provider
and
social_auth
.
uid
.
startswith
(
prefix
)
return
self
.
backend_name
==
social_auth
.
provider
and
social_auth
.
uid
.
startswith
(
prefix
)
def
get_remote_id_from_social_auth
(
self
,
social_auth
):
""" Given a UserSocialAuth object, return the remote ID used by this provider. """
assert
self
.
match_social_auth
(
social_auth
)
# Remove the prefix from the UID
return
social_auth
.
uid
[
len
(
self
.
idp_slug
)
+
1
:]
def
get_config
(
self
):
def
get_config
(
self
):
"""
"""
Return a SAMLIdentityProvider instance for use by SAMLAuthBackend.
Return a SAMLIdentityProvider instance for use by SAMLAuthBackend.
...
...
common/djangoapps/third_party_auth/pipeline.py
View file @
a8d1d663
...
@@ -56,6 +56,10 @@ rather than spreading them across two functions in the pipeline.
...
@@ -56,6 +56,10 @@ rather than spreading them across two functions in the pipeline.
See http://psa.matiasaguirre.net/docs/pipeline.html for more docs.
See http://psa.matiasaguirre.net/docs/pipeline.html for more docs.
"""
"""
import
base64
import
hashlib
import
hmac
import
json
import
random
import
random
import
string
# pylint: disable-msg=deprecated-module
import
string
# pylint: disable-msg=deprecated-module
from
collections
import
OrderedDict
from
collections
import
OrderedDict
...
@@ -111,6 +115,9 @@ AUTH_ENTRY_REGISTER_2 = 'account_register'
...
@@ -111,6 +115,9 @@ AUTH_ENTRY_REGISTER_2 = 'account_register'
AUTH_ENTRY_LOGIN_API
=
'login_api'
AUTH_ENTRY_LOGIN_API
=
'login_api'
AUTH_ENTRY_REGISTER_API
=
'register_api'
AUTH_ENTRY_REGISTER_API
=
'register_api'
# Custom auth entry point used by external software
AUTH_ENTRY_CUSTOM
=
getattr
(
settings
,
'THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS'
,
{})
def
is_api
(
auth_entry
):
def
is_api
(
auth_entry
):
"""Returns whether the auth entry point is via an API call."""
"""Returns whether the auth entry point is via an API call."""
...
@@ -151,7 +158,7 @@ _AUTH_ENTRY_CHOICES = frozenset([
...
@@ -151,7 +158,7 @@ _AUTH_ENTRY_CHOICES = frozenset([
AUTH_ENTRY_LOGIN_API
,
AUTH_ENTRY_LOGIN_API
,
AUTH_ENTRY_REGISTER_API
,
AUTH_ENTRY_REGISTER_API
,
])
]
+
AUTH_ENTRY_CUSTOM
.
keys
()
)
_DEFAULT_RANDOM_PASSWORD_LENGTH
=
12
_DEFAULT_RANDOM_PASSWORD_LENGTH
=
12
_PASSWORD_CHARSET
=
string
.
letters
+
string
.
digits
_PASSWORD_CHARSET
=
string
.
letters
+
string
.
digits
...
@@ -208,11 +215,17 @@ class ProviderUserState(object):
...
@@ -208,11 +215,17 @@ class ProviderUserState(object):
lms/templates/dashboard.html.
lms/templates/dashboard.html.
"""
"""
def
__init__
(
self
,
enabled_provider
,
user
,
association_id
=
None
):
def
__init__
(
self
,
enabled_provider
,
user
,
association
):
# UserSocialAuth row ID
self
.
association_id
=
association_id
# Boolean. Whether the user has an account associated with the provider
# Boolean. Whether the user has an account associated with the provider
self
.
has_account
=
association_id
is
not
None
self
.
has_account
=
association
is
not
None
if
self
.
has_account
:
# UserSocialAuth row ID
self
.
association_id
=
association
.
id
# Identifier of this user according to the remote provider:
self
.
remote_id
=
enabled_provider
.
get_remote_id_from_social_auth
(
association
)
else
:
self
.
association_id
=
None
self
.
remote_id
=
None
# provider.BaseProvider child. Callers must verify that the provider is
# provider.BaseProvider child. Callers must verify that the provider is
# enabled.
# enabled.
self
.
provider
=
enabled_provider
self
.
provider
=
enabled_provider
...
@@ -405,13 +418,13 @@ def get_provider_user_states(user):
...
@@ -405,13 +418,13 @@ def get_provider_user_states(user):
found_user_auths
=
list
(
models
.
DjangoStorage
.
user
.
get_social_auth_for_user
(
user
))
found_user_auths
=
list
(
models
.
DjangoStorage
.
user
.
get_social_auth_for_user
(
user
))
for
enabled_provider
in
provider
.
Registry
.
enabled
():
for
enabled_provider
in
provider
.
Registry
.
enabled
():
association
_id
=
None
association
=
None
for
auth
in
found_user_auths
:
for
auth
in
found_user_auths
:
if
enabled_provider
.
match_social_auth
(
auth
):
if
enabled_provider
.
match_social_auth
(
auth
):
association
_id
=
auth
.
id
association
=
auth
break
break
states
.
append
(
states
.
append
(
ProviderUserState
(
enabled_provider
,
user
,
association
_id
)
ProviderUserState
(
enabled_provider
,
user
,
association
)
)
)
return
states
return
states
...
@@ -488,6 +501,33 @@ def set_pipeline_timeout(strategy, user, *args, **kwargs):
...
@@ -488,6 +501,33 @@ def set_pipeline_timeout(strategy, user, *args, **kwargs):
# choice of the user.
# choice of the user.
def
redirect_to_custom_form
(
request
,
auth_entry
,
user_details
):
"""
If auth_entry is found in AUTH_ENTRY_CUSTOM, this is used to send provider
data to an external server's registration/login page.
The data is sent as a base64-encoded values in a POST request and includes
a cryptographic checksum in case the integrity of the data is important.
"""
form_info
=
AUTH_ENTRY_CUSTOM
[
auth_entry
]
secret_key
=
form_info
[
'secret_key'
]
if
isinstance
(
secret_key
,
unicode
):
secret_key
=
secret_key
.
encode
(
'utf-8'
)
custom_form_url
=
form_info
[
'url'
]
data_str
=
json
.
dumps
({
"user_details"
:
user_details
})
digest
=
hmac
.
new
(
secret_key
,
msg
=
data_str
,
digestmod
=
hashlib
.
sha256
)
.
digest
()
# Store the data in the session temporarily, then redirect to a page that will POST it to
# the custom login/register page.
request
.
session
[
'tpa_custom_auth_entry_data'
]
=
{
'data'
:
base64
.
b64encode
(
data_str
),
'hmac'
:
base64
.
b64encode
(
digest
),
'post_url'
:
custom_form_url
,
}
return
redirect
(
reverse
(
'tpa_post_to_custom_auth_form'
))
@partial.partial
@partial.partial
def
ensure_user_information
(
strategy
,
auth_entry
,
backend
=
None
,
user
=
None
,
social
=
None
,
def
ensure_user_information
(
strategy
,
auth_entry
,
backend
=
None
,
user
=
None
,
social
=
None
,
allow_inactive_user
=
False
,
*
args
,
**
kwargs
):
allow_inactive_user
=
False
,
*
args
,
**
kwargs
):
...
@@ -551,6 +591,9 @@ def ensure_user_information(strategy, auth_entry, backend=None, user=None, socia
...
@@ -551,6 +591,9 @@ def ensure_user_information(strategy, auth_entry, backend=None, user=None, socia
return
dispatch_to_register
()
return
dispatch_to_register
()
elif
auth_entry
==
AUTH_ENTRY_ACCOUNT_SETTINGS
:
elif
auth_entry
==
AUTH_ENTRY_ACCOUNT_SETTINGS
:
raise
AuthEntryError
(
backend
,
'auth_entry is wrong. Settings requires a user.'
)
raise
AuthEntryError
(
backend
,
'auth_entry is wrong. Settings requires a user.'
)
elif
auth_entry
in
AUTH_ENTRY_CUSTOM
:
# Pass the username, email, etc. via query params to the custom entry page:
return
redirect_to_custom_form
(
strategy
.
request
,
auth_entry
,
kwargs
[
'details'
])
else
:
else
:
raise
AuthEntryError
(
backend
,
'auth_entry invalid'
)
raise
AuthEntryError
(
backend
,
'auth_entry invalid'
)
...
...
common/djangoapps/third_party_auth/settings.py
View file @
a8d1d663
...
@@ -93,3 +93,6 @@ def apply_settings(django_settings):
...
@@ -93,3 +93,6 @@ def apply_settings(django_settings):
django_settings
.
SOCIAL_AUTH_USER_FIELDS
=
getattr
(
django_settings
.
SOCIAL_AUTH_USER_FIELDS
=
getattr
(
django_settings
,
'USER_FIELDS'
,
[
'username'
,
'email'
,
'first_name'
,
'last_name'
,
'fullname'
]
django_settings
,
'USER_FIELDS'
,
[
'username'
,
'email'
,
'first_name'
,
'last_name'
,
'fullname'
]
)
)
if
not
hasattr
(
django_settings
,
'THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS'
):
django_settings
.
THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS
=
{}
common/djangoapps/third_party_auth/strategy.py
View file @
a8d1d663
...
@@ -4,6 +4,7 @@ ConfigurationModels rather than django.settings
...
@@ -4,6 +4,7 @@ ConfigurationModels rather than django.settings
"""
"""
import
logging
import
logging
from
.models
import
OAuth2ProviderConfig
from
.models
import
OAuth2ProviderConfig
from
.pipeline
import
AUTH_ENTRY_CUSTOM
from
social.backends.oauth
import
BaseOAuth2
from
social.backends.oauth
import
BaseOAuth2
from
social.strategies.django_strategy
import
DjangoStrategy
from
social.strategies.django_strategy
import
DjangoStrategy
...
@@ -33,6 +34,15 @@ class ConfigurationModelStrategy(DjangoStrategy):
...
@@ -33,6 +34,15 @@ class ConfigurationModelStrategy(DjangoStrategy):
return
provider_config
.
get_setting
(
name
)
return
provider_config
.
get_setting
(
name
)
except
KeyError
:
except
KeyError
:
pass
pass
# special case handling of login error URL if we're using a custom auth entry point:
if
name
==
'LOGIN_ERROR_URL'
:
auth_entry
=
self
.
request
.
session
.
get
(
'auth_entry'
)
if
auth_entry
and
auth_entry
in
AUTH_ENTRY_CUSTOM
:
error_url
=
AUTH_ENTRY_CUSTOM
[
auth_entry
]
.
get
(
'error_url'
)
if
error_url
:
return
error_url
# At this point, we know 'name' is not set in a [OAuth2|SAML]ProviderConfig row.
# At this point, we know 'name' is not set in a [OAuth2|SAML]ProviderConfig row.
# It's probably a global Django setting like 'FIELDS_STORED_IN_SESSION':
# It's probably a global Django setting like 'FIELDS_STORED_IN_SESSION':
return
super
(
ConfigurationModelStrategy
,
self
)
.
setting
(
name
,
default
,
backend
)
return
super
(
ConfigurationModelStrategy
,
self
)
.
setting
(
name
,
default
,
backend
)
...
@@ -77,3 +87,29 @@ class ConfigurationModelStrategy(DjangoStrategy):
...
@@ -77,3 +87,29 @@ class ConfigurationModelStrategy(DjangoStrategy):
# when autoprovisioning we need to skip email activation, hence skip_email is True
# when autoprovisioning we need to skip email activation, hence skip_email is True
return
create_account_with_params
(
self
.
request
,
user_fields
,
skip_email
=
True
)
return
create_account_with_params
(
self
.
request
,
user_fields
,
skip_email
=
True
)
def
request_host
(
self
):
"""
Host in use for this request
"""
# TODO: this override is a temporary measure until upstream python-social-auth patch is merged:
# https://github.com/omab/python-social-auth/pull/741
if
self
.
setting
(
'RESPECT_X_FORWARDED_HEADERS'
,
False
):
forwarded_host
=
self
.
request
.
META
.
get
(
'HTTP_X_FORWARDED_HOST'
)
if
forwarded_host
:
return
forwarded_host
return
super
(
ConfigurationModelStrategy
,
self
)
.
request_host
()
def
request_port
(
self
):
"""
Port in use for this request
"""
# TODO: this override is a temporary measure until upstream python-social-auth patch is merged:
# https://github.com/omab/python-social-auth/pull/741
if
self
.
setting
(
'RESPECT_X_FORWARDED_HEADERS'
,
False
):
forwarded_port
=
self
.
request
.
META
.
get
(
'HTTP_X_FORWARDED_PORT'
)
if
forwarded_port
:
return
forwarded_port
return
super
(
ConfigurationModelStrategy
,
self
)
.
request_port
()
common/djangoapps/third_party_auth/templates/third_party_auth/post_custom_auth_entry.html
0 → 100644
View file @
a8d1d663
{% load i18n %}
<!DOCTYPE html>
<html
lang=
"en"
>
<head>
<title>
{% trans "Please wait" %}
</title>
<style
type=
"text/css"
>
#djDebug
{
display
:
none
;}
</style>
</head>
<body>
<form
id=
"sso-data-form"
action=
"{{post_url}}"
method=
"post"
>
{% csrf_token %}
<input
type=
"hidden"
name=
"sso_data"
value=
"{{data}}"
>
<input
type=
"hidden"
name=
"sso_data_hmac"
value=
"{{hmac}}"
>
<noscript>
<input
id=
"submit-button"
type=
"submit"
value=
"Click to continue"
autofocus
>
</noscript>
</form>
<script>
document
.
getElementById
(
'sso-data-form'
).
submit
();
</script>
</body>
</html>
common/djangoapps/third_party_auth/tests/specs/test_google.py
View file @
a8d1d663
"""Integration tests for Google providers."""
"""Integration tests for Google providers."""
from
third_party_auth
import
provider
import
base64
import
hashlib
import
hmac
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
import
json
from
mock
import
patch
from
social.exceptions
import
AuthException
from
student.tests.factories
import
UserFactory
from
third_party_auth
import
pipeline
from
third_party_auth.tests.specs
import
base
from
third_party_auth.tests.specs
import
base
...
@@ -35,3 +44,90 @@ class GoogleOauth2IntegrationTest(base.Oauth2IntegrationTest):
...
@@ -35,3 +44,90 @@ class GoogleOauth2IntegrationTest(base.Oauth2IntegrationTest):
def
get_username
(
self
):
def
get_username
(
self
):
return
self
.
get_response_data
()
.
get
(
'email'
)
.
split
(
'@'
)[
0
]
return
self
.
get_response_data
()
.
get
(
'email'
)
.
split
(
'@'
)[
0
]
def
assert_redirect_to_provider_looks_correct
(
self
,
response
):
super
(
GoogleOauth2IntegrationTest
,
self
)
.
assert_redirect_to_provider_looks_correct
(
response
)
self
.
assertIn
(
'google.com'
,
response
[
'Location'
])
def
test_custom_form
(
self
):
"""
Use the Google provider to test the custom login/register form feature.
"""
# The pipeline starts by a user GETting /auth/login/google-oauth2/?auth_entry=custom1
# Synthesize that request and check that it redirects to the correct
# provider page.
auth_entry
=
'custom1'
# See definition in lms/envs/test.py
login_url
=
pipeline
.
get_login_url
(
self
.
provider
.
provider_id
,
auth_entry
)
login_url
+=
"&next=/misc/final-destination"
self
.
assert_redirect_to_provider_looks_correct
(
self
.
client
.
get
(
login_url
))
def
fake_auth_complete
(
inst
,
*
args
,
**
kwargs
):
""" Mock the backend's auth_complete() method """
kwargs
.
update
({
'response'
:
self
.
get_response_data
(),
'backend'
:
inst
})
return
inst
.
strategy
.
authenticate
(
*
args
,
**
kwargs
)
# Next, the provider makes a request against /auth/complete/<provider>.
complete_url
=
pipeline
.
get_complete_url
(
self
.
provider
.
backend_name
)
with
patch
.
object
(
self
.
provider
.
backend_class
,
'auth_complete'
,
fake_auth_complete
):
response
=
self
.
client
.
get
(
complete_url
)
# This should redirect to the custom login/register form:
self
.
assertEqual
(
response
.
status_code
,
302
)
self
.
assertEqual
(
response
[
'Location'
],
'http://testserver/auth/custom_auth_entry'
)
response
=
self
.
client
.
get
(
response
[
'Location'
])
self
.
assertEqual
(
response
.
status_code
,
200
)
self
.
assertIn
(
'action="/misc/my-custom-registration-form" method="post"'
,
response
.
content
)
data_decoded
=
base64
.
b64decode
(
response
.
context
[
'data'
])
# pylint: disable=no-member
data_parsed
=
json
.
loads
(
data_decoded
)
# The user's details get passed to the custom page as a base64 encoded query parameter:
self
.
assertEqual
(
data_parsed
,
{
'user_details'
:
{
'username'
:
'email_value'
,
'email'
:
'email_value@example.com'
,
'fullname'
:
'name_value'
,
'first_name'
:
'given_name_value'
,
'last_name'
:
'family_name_value'
,
}
})
# Check the hash that is used to confirm the user's data in the GET parameter is correct
secret_key
=
settings
.
THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS
[
'custom1'
][
'secret_key'
]
hmac_expected
=
hmac
.
new
(
secret_key
,
msg
=
data_decoded
,
digestmod
=
hashlib
.
sha256
)
.
digest
()
self
.
assertEqual
(
base64
.
b64decode
(
response
.
context
[
'hmac'
]),
hmac_expected
)
# pylint: disable=no-member
# Now our custom registration form creates or logs in the user:
email
,
password
=
data_parsed
[
'user_details'
][
'email'
],
'random_password'
created_user
=
UserFactory
(
email
=
email
,
password
=
password
)
login_response
=
self
.
client
.
post
(
reverse
(
'login'
),
{
'email'
:
email
,
'password'
:
password
})
self
.
assertEqual
(
login_response
.
status_code
,
200
)
# Now our custom login/registration page must resume the pipeline:
response
=
self
.
client
.
get
(
complete_url
)
self
.
assertEqual
(
response
.
status_code
,
302
)
self
.
assertEqual
(
response
[
'Location'
],
'http://testserver/misc/final-destination'
)
_
,
strategy
=
self
.
get_request_and_strategy
()
self
.
assert_social_auth_exists_for_user
(
created_user
,
strategy
)
def
test_custom_form_error
(
self
):
"""
Use the Google provider to test the custom login/register failure redirects.
"""
# The pipeline starts by a user GETting /auth/login/google-oauth2/?auth_entry=custom1
# Synthesize that request and check that it redirects to the correct
# provider page.
auth_entry
=
'custom1'
# See definition in lms/envs/test.py
login_url
=
pipeline
.
get_login_url
(
self
.
provider
.
provider_id
,
auth_entry
)
login_url
+=
"&next=/misc/final-destination"
self
.
assert_redirect_to_provider_looks_correct
(
self
.
client
.
get
(
login_url
))
def
fake_auth_complete_error
(
_inst
,
*
_args
,
**
_kwargs
):
""" Mock the backend's auth_complete() method """
raise
AuthException
(
"Mock login failed"
)
# Next, the provider makes a request against /auth/complete/<provider>.
complete_url
=
pipeline
.
get_complete_url
(
self
.
provider
.
backend_name
)
with
patch
.
object
(
self
.
provider
.
backend_class
,
'auth_complete'
,
fake_auth_complete_error
):
response
=
self
.
client
.
get
(
complete_url
)
# This should redirect to the custom error URL
self
.
assertEqual
(
response
.
status_code
,
302
)
self
.
assertEqual
(
response
[
'Location'
],
'http://testserver/misc/my-custom-sso-error-page'
)
common/djangoapps/third_party_auth/tests/test_pipeline.py
View file @
a8d1d663
...
@@ -43,7 +43,7 @@ class ProviderUserStateTestCase(testutil.TestCase):
...
@@ -43,7 +43,7 @@ class ProviderUserStateTestCase(testutil.TestCase):
def
test_get_unlink_form_name
(
self
):
def
test_get_unlink_form_name
(
self
):
google_provider
=
self
.
configure_google_provider
(
enabled
=
True
)
google_provider
=
self
.
configure_google_provider
(
enabled
=
True
)
state
=
pipeline
.
ProviderUserState
(
google_provider
,
object
(),
1000
)
state
=
pipeline
.
ProviderUserState
(
google_provider
,
object
(),
None
)
self
.
assertEqual
(
google_provider
.
provider_id
+
'_unlink_form'
,
state
.
get_unlink_form_name
())
self
.
assertEqual
(
google_provider
.
provider_id
+
'_unlink_form'
,
state
.
get_unlink_form_name
())
...
...
common/djangoapps/third_party_auth/tests/test_strategy.py
View file @
a8d1d663
...
@@ -2,7 +2,9 @@
...
@@ -2,7 +2,9 @@
import
unittest
import
unittest
import
ddt
import
ddt
import
mock
import
mock
from
unittest
import
TestCase
from
django.test
import
TestCase
from
third_party_auth.strategy
import
ConfigurationModelStrategy
from
third_party_auth.strategy
import
ConfigurationModelStrategy
from
third_party_auth.tests
import
testutil
from
third_party_auth.tests
import
testutil
...
@@ -95,3 +97,42 @@ class TestStrategy(TestCase):
...
@@ -95,3 +97,42 @@ class TestStrategy(TestCase):
_
,
user_data
=
self
.
_get_last_call_args
(
patched_create_account
)
_
,
user_data
=
self
.
_get_last_call_args
(
patched_create_account
)
self
.
assertEqual
(
user_data
[
'email'
],
expected_email
)
self
.
assertEqual
(
user_data
[
'email'
],
expected_email
)
@ddt.data
(
(
True
,
None
,
'host'
,
'host'
),
(
True
,
""
,
'other_host'
,
'other_host'
),
(
True
,
'x_forwarded_host'
,
'irrelevant'
,
'x_forwarded_host'
),
(
True
,
'other_x_forwarded_host'
,
'still_irrelevant'
,
'other_x_forwarded_host'
),
(
False
,
None
,
'host'
,
'host'
),
(
False
,
""
,
'other_host'
,
'other_host'
),
(
False
,
'x_forwarded_host'
,
'normal_host'
,
'normal_host'
),
(
False
,
'other_x_forwarded_host'
,
'other_normal_host'
,
'other_normal_host'
),
)
@ddt.unpack
def
test_request_host
(
self
,
respect_x_headers
,
x_forwarded_value
,
get_host_value
,
expected_value
,
unused_patch
):
self
.
request_mock
.
META
=
{}
self
.
request_mock
.
get_host
.
return_value
=
get_host_value
if
x_forwarded_value
is
not
None
:
self
.
request_mock
.
META
[
'HTTP_X_FORWARDED_HOST'
]
=
x_forwarded_value
with
self
.
settings
(
RESPECT_X_FORWARDED_HEADERS
=
respect_x_headers
):
self
.
assertEqual
(
self
.
strategy
.
request_host
(),
expected_value
)
@ddt.data
(
(
True
,
None
,
'port'
,
'port'
),
(
True
,
""
,
'other_port'
,
'other_port'
),
(
True
,
'x_forwarded_port'
,
'irrelevant'
,
'x_forwarded_port'
),
(
True
,
'other_x_forwarded_port'
,
'still_irrelevant'
,
'other_x_forwarded_port'
),
(
False
,
None
,
'port'
,
'port'
),
(
False
,
""
,
'other_port'
,
'other_port'
),
(
False
,
'x_forwarded_port'
,
'normal_port'
,
'normal_port'
),
(
False
,
'other_x_forwarded_port'
,
'other_normal_port'
,
'other_normal_port'
),
)
@ddt.unpack
def
test_request_port
(
self
,
respect_x_headers
,
x_forwarded_value
,
server_port_value
,
expected_value
,
unused_patch
):
self
.
request_mock
.
META
=
{
'SERVER_PORT'
:
server_port_value
}
if
x_forwarded_value
is
not
None
:
self
.
request_mock
.
META
[
'HTTP_X_FORWARDED_PORT'
]
=
x_forwarded_value
with
self
.
settings
(
RESPECT_X_FORWARDED_HEADERS
=
respect_x_headers
):
self
.
assertEqual
(
self
.
strategy
.
request_port
(),
expected_value
)
common/djangoapps/third_party_auth/urls.py
View file @
a8d1d663
...
@@ -2,11 +2,12 @@
...
@@ -2,11 +2,12 @@
from
django.conf.urls
import
include
,
patterns
,
url
from
django.conf.urls
import
include
,
patterns
,
url
from
.views
import
inactive_user_view
,
saml_metadata_view
from
.views
import
inactive_user_view
,
saml_metadata_view
,
post_to_custom_auth_form
urlpatterns
=
patterns
(
urlpatterns
=
patterns
(
''
,
''
,
url
(
r'^auth/inactive'
,
inactive_user_view
),
url
(
r'^auth/inactive'
,
inactive_user_view
),
url
(
r'^auth/custom_auth_entry'
,
post_to_custom_auth_form
,
name
=
'tpa_post_to_custom_auth_form'
),
url
(
r'^auth/saml/metadata.xml'
,
saml_metadata_view
),
url
(
r'^auth/saml/metadata.xml'
,
saml_metadata_view
),
url
(
r'^auth/'
,
include
(
'social.apps.django_app.urls'
,
namespace
=
'social'
)),
url
(
r'^auth/'
,
include
(
'social.apps.django_app.urls'
,
namespace
=
'social'
)),
)
)
common/djangoapps/third_party_auth/views.py
View file @
a8d1d663
...
@@ -4,7 +4,7 @@ Extra views required for SSO
...
@@ -4,7 +4,7 @@ Extra views required for SSO
from
django.conf
import
settings
from
django.conf
import
settings
from
django.core.urlresolvers
import
reverse
from
django.core.urlresolvers
import
reverse
from
django.http
import
HttpResponse
,
HttpResponseServerError
,
Http404
from
django.http
import
HttpResponse
,
HttpResponseServerError
,
Http404
from
django.shortcuts
import
redirect
from
django.shortcuts
import
redirect
,
render
from
social.apps.django_app.utils
import
load_strategy
,
load_backend
from
social.apps.django_app.utils
import
load_strategy
,
load_backend
from
.models
import
SAMLConfiguration
from
.models
import
SAMLConfiguration
...
@@ -36,3 +36,26 @@ def saml_metadata_view(request):
...
@@ -36,3 +36,26 @@ def saml_metadata_view(request):
if
not
errors
:
if
not
errors
:
return
HttpResponse
(
content
=
metadata
,
content_type
=
'text/xml'
)
return
HttpResponse
(
content
=
metadata
,
content_type
=
'text/xml'
)
return
HttpResponseServerError
(
content
=
', '
.
join
(
errors
))
return
HttpResponseServerError
(
content
=
', '
.
join
(
errors
))
def
post_to_custom_auth_form
(
request
):
"""
Redirect to a custom login/register page.
Since we can't do a redirect-to-POST, this view is used to pass SSO data from
the third_party_auth pipeline to a custom login/register form (possibly on another server).
"""
pipeline_data
=
request
.
session
.
pop
(
'tpa_custom_auth_entry_data'
,
None
)
if
not
pipeline_data
:
raise
Http404
# Verify the format of pipeline_data:
data
=
{
'post_url'
:
pipeline_data
[
'post_url'
],
# The user's name, email, etc. as base64 encoded JSON
# It's base64 encoded because it's signed cryptographically and we don't want whitespace
# or ordering issues affecting the hash/signature.
'data'
:
pipeline_data
[
'data'
],
# The cryptographic hash of user_data:
'hmac'
:
pipeline_data
[
'hmac'
],
}
return
render
(
request
,
'third_party_auth/post_custom_auth_entry.html'
,
data
)
lms/djangoapps/api_manager/sessions/views.py
View file @
a8d1d663
...
@@ -55,6 +55,12 @@ class SessionsList(SecureAPIView):
...
@@ -55,6 +55,12 @@ class SessionsList(SecureAPIView):
"""
"""
def
post
(
self
,
request
):
def
post
(
self
,
request
):
return
self
.
login_user
(
request
)
# pylint: disable=too-many-statements
@staticmethod
def
login_user
(
request
,
session_id
=
None
):
""" Create a new session and login the user, or upgrade an existing session """
response_data
=
{}
response_data
=
{}
# Add some rate limiting here by re-using the RateLimitMixin as a helper class
# Add some rate limiting here by re-using the RateLimitMixin as a helper class
limiter
=
BadRequestRateLimiter
()
limiter
=
BadRequestRateLimiter
()
...
@@ -105,21 +111,34 @@ class SessionsList(SecureAPIView):
...
@@ -105,21 +111,34 @@ class SessionsList(SecureAPIView):
# violate our RESTfulness
# violate our RESTfulness
#
#
engine
=
import_module
(
settings
.
SESSION_ENGINE
)
engine
=
import_module
(
settings
.
SESSION_ENGINE
)
new_session
=
engine
.
SessionStore
()
if
session_id
is
None
:
new_session
.
create
()
session
=
engine
.
SessionStore
()
session
.
create
()
success_status
=
status
.
HTTP_201_CREATED
else
:
session
=
engine
.
SessionStore
(
session_id
)
success_status
=
status
.
HTTP_200_OK
if
SESSION_KEY
in
session
:
# Someone is already logged in. The user ID of whoever is logged in
# now might be different than the user ID we've been asked to login,
# which would be bad. But even if it is the same user, we should not
# be asked to login a user who is already logged in. This likely
# indicates some sort of programming/validation error and possibly
# even a potential security issue - so return 403.
return
Response
({},
status
=
status
.
HTTP_403_FORBIDDEN
)
# These values are expected to be set in any new session
# These values are expected to be set in any new session
new_
session
[
SESSION_KEY
]
=
user
.
id
session
[
SESSION_KEY
]
=
user
.
id
new_
session
[
BACKEND_SESSION_KEY
]
=
user
.
backend
session
[
BACKEND_SESSION_KEY
]
=
user
.
backend
new_
session
.
save
()
session
.
save
()
response_data
[
'token'
]
=
new_
session
.
session_key
response_data
[
'token'
]
=
session
.
session_key
response_data
[
'expires'
]
=
new_
session
.
get_expiry_age
()
response_data
[
'expires'
]
=
session
.
get_expiry_age
()
user_dto
=
UserSerializer
(
user
)
user_dto
=
UserSerializer
(
user
)
response_data
[
'user'
]
=
user_dto
.
data
response_data
[
'user'
]
=
user_dto
.
data
response_data
[
'uri'
]
=
'{}/{}'
.
format
(
base_uri
,
new_
session
.
session_key
)
response_data
[
'uri'
]
=
'{}/{}'
.
format
(
base_uri
,
session
.
session_key
)
response_status
=
s
tatus
.
HTTP_201_CREATED
response_status
=
s
uccess_status
# generate a CSRF tokens for any web clients that may need to
# generate a CSRF tokens for any web clients that may need to
# call into the LMS via Ajax (for example Notifications)
# call into the LMS via Ajax (for example Notifications)
...
@@ -152,13 +171,16 @@ class SessionsDetail(SecureAPIView):
...
@@ -152,13 +171,16 @@ class SessionsDetail(SecureAPIView):
**Use Case**
**Use Case**
SessionsDetail gets a details about a specific API session, as well as
SessionsDetail gets a details about a specific API session, as well as
enables you to delete an API session.
enables you to delete an API session or "upgrade" a session by logging
in the user.
**Example Requests**
**Example Requests**
GET /api/session/{session_id}
GET /api/session/{session_id}
POST /api/session/{session_id}
DELETE /api/session/{session_id}/delete
DELETE /api/session/{session_id}/delete
**GET Response Values**
**GET Response Values**
...
@@ -190,6 +212,10 @@ class SessionsDetail(SecureAPIView):
...
@@ -190,6 +212,10 @@ class SessionsDetail(SecureAPIView):
else
:
else
:
return
Response
(
response_data
,
status
=
status
.
HTTP_404_NOT_FOUND
)
return
Response
(
response_data
,
status
=
status
.
HTTP_404_NOT_FOUND
)
def
post
(
self
,
request
,
session_id
):
""" Login and upgrade an existing session from anonymous to authenticated. """
return
SessionsList
.
login_user
(
request
,
session_id
)
def
delete
(
self
,
request
,
session_id
):
def
delete
(
self
,
request
,
session_id
):
engine
=
import_module
(
settings
.
SESSION_ENGINE
)
engine
=
import_module
(
settings
.
SESSION_ENGINE
)
session
=
engine
.
SessionStore
(
session_id
)
session
=
engine
.
SessionStore
(
session_id
)
...
...
lms/djangoapps/api_manager/users/tests.py
View file @
a8d1d663
...
@@ -377,9 +377,11 @@ class UsersApiTests(ModuleStoreTestCase):
...
@@ -377,9 +377,11 @@ class UsersApiTests(ModuleStoreTestCase):
data
=
{
'email'
:
self
.
test_email
,
'username'
:
local_username
,
'password'
:
data
=
{
'email'
:
self
.
test_email
,
'username'
:
local_username
,
'password'
:
self
.
test_password
,
'first_name'
:
self
.
test_first_name
,
'last_name'
:
self
.
test_last_name
}
self
.
test_password
,
'first_name'
:
self
.
test_first_name
,
'last_name'
:
self
.
test_last_name
}
response
=
self
.
do_post
(
test_uri
,
data
)
response
=
self
.
do_post
(
test_uri
,
data
)
response
=
self
.
do_post
(
test_uri
,
data
)
expected_message
=
"Username '{username}' or email '{email}' already exists"
.
format
(
username
=
local_username
,
email
=
self
.
test_email
)
self
.
assertEqual
(
response
.
status_code
,
409
)
self
.
assertEqual
(
response
.
status_code
,
409
)
self
.
assert
Greater
(
response
.
data
[
'message'
],
0
)
self
.
assert
Equal
(
response
.
data
[
'message'
],
expected_message
)
self
.
assertEqual
(
response
.
data
[
'field_conflict'
],
'username or email'
)
self
.
assertEqual
(
response
.
data
[
'field_conflict'
],
'username or email'
)
@mock.patch.dict
(
"student.models.settings.FEATURES"
,
{
"ENABLE_DISCUSSION_EMAIL_DIGEST"
:
True
})
@mock.patch.dict
(
"student.models.settings.FEATURES"
,
{
"ENABLE_DISCUSSION_EMAIL_DIGEST"
:
True
})
...
...
lms/djangoapps/api_manager/users/urls.py
View file @
a8d1d663
...
@@ -17,10 +17,6 @@ urlpatterns = patterns(
...
@@ -17,10 +17,6 @@ urlpatterns = patterns(
url
(
r'^(?P<user_id>[a-zA-Z0-9]+)/courses/{0}/metrics/social/$'
.
format
(
COURSE_ID_PATTERN
),
users_views
.
UsersSocialMetrics
.
as_view
(),
name
=
'users-social-metrics'
),
url
(
r'^(?P<user_id>[a-zA-Z0-9]+)/courses/{0}/metrics/social/$'
.
format
(
COURSE_ID_PATTERN
),
users_views
.
UsersSocialMetrics
.
as_view
(),
name
=
'users-social-metrics'
),
url
(
r'^(?P<user_id>[a-zA-Z0-9]+)/courses/{0}/completions/$'
.
format
(
COURSE_ID_PATTERN
),
users_views
.
UsersCoursesCompletionsList
.
as_view
(),
name
=
'users-courses-completions-list'
),
url
(
r'^(?P<user_id>[a-zA-Z0-9]+)/courses/{0}/completions/$'
.
format
(
COURSE_ID_PATTERN
),
users_views
.
UsersCoursesCompletionsList
.
as_view
(),
name
=
'users-courses-completions-list'
),
url
(
r'^(?P<user_id>[a-zA-Z0-9]+)/courses/{0}$'
.
format
(
COURSE_ID_PATTERN
),
users_views
.
UsersCoursesDetail
.
as_view
(),
name
=
'users-courses-detail'
),
url
(
r'^(?P<user_id>[a-zA-Z0-9]+)/courses/{0}$'
.
format
(
COURSE_ID_PATTERN
),
users_views
.
UsersCoursesDetail
.
as_view
(),
name
=
'users-courses-detail'
),
url
(
r'^(?P<user_id>[a-zA-Z0-9]+)/courses/{0}/grades$'
.
format
(
COURSE_ID_PATTERN
),
users_views
.
UsersCoursesGradesDetail
.
as_view
(),
name
=
'users-courses-grades-detail'
),
url
(
r'^(?P<user_id>[a-zA-Z0-9]+)/courses/{0}/metrics/social/$'
.
format
(
COURSE_ID_PATTERN
),
users_views
.
UsersSocialMetrics
.
as_view
(),
name
=
'users-social-metrics'
),
url
(
r'^(?P<user_id>[a-zA-Z0-9]+)/courses/{0}/completions/$'
.
format
(
COURSE_ID_PATTERN
),
users_views
.
UsersCoursesCompletionsList
.
as_view
(),
name
=
'users-courses-completions-list'
),
url
(
r'^(?P<user_id>[a-zA-Z0-9]+)/courses/{0}$'
.
format
(
COURSE_ID_PATTERN
),
users_views
.
UsersCoursesDetail
.
as_view
(),
name
=
'users-courses-detail'
),
url
(
r'^(?P<user_id>[a-zA-Z0-9]+)/courses/*$'
,
users_views
.
UsersCoursesList
.
as_view
(),
name
=
'users-courses-list'
),
url
(
r'^(?P<user_id>[a-zA-Z0-9]+)/courses/*$'
,
users_views
.
UsersCoursesList
.
as_view
(),
name
=
'users-courses-list'
),
url
(
r'^(?P<user_id>[a-zA-Z0-9]+)/groups/*$'
,
users_views
.
UsersGroupsList
.
as_view
(),
name
=
'users-groups-list'
),
url
(
r'^(?P<user_id>[a-zA-Z0-9]+)/groups/*$'
,
users_views
.
UsersGroupsList
.
as_view
(),
name
=
'users-groups-list'
),
url
(
r'^(?P<user_id>[a-zA-Z0-9]+)/groups/(?P<group_id>[0-9]+)$'
,
users_views
.
UsersGroupsDetail
.
as_view
(),
name
=
'users-groups-detail'
),
url
(
r'^(?P<user_id>[a-zA-Z0-9]+)/groups/(?P<group_id>[0-9]+)$'
,
users_views
.
UsersGroupsDetail
.
as_view
(),
name
=
'users-groups-detail'
),
...
...
lms/djangoapps/api_manager/users/views.py
View file @
a8d1d663
...
@@ -325,7 +325,9 @@ class UsersList(SecureListAPIView):
...
@@ -325,7 +325,9 @@ class UsersList(SecureListAPIView):
try
:
try
:
user
=
User
.
objects
.
create
(
email
=
email
,
username
=
username
,
is_staff
=
is_staff
)
user
=
User
.
objects
.
create
(
email
=
email
,
username
=
username
,
is_staff
=
is_staff
)
except
IntegrityError
:
except
IntegrityError
:
response_data
[
'message'
]
=
"User '
%
s' already exists"
%
(
username
)
response_data
[
'message'
]
=
_
(
"Username '{username}' or email '{email}' already exists"
)
.
format
(
username
=
username
,
email
=
email
)
response_data
[
'field_conflict'
]
=
"username or email"
response_data
[
'field_conflict'
]
=
"username or email"
return
Response
(
response_data
,
status
=
status
.
HTTP_409_CONFLICT
)
return
Response
(
response_data
,
status
=
status
.
HTTP_409_CONFLICT
)
...
...
lms/djangoapps/gradebook/receivers.py
View file @
a8d1d663
...
@@ -26,7 +26,7 @@ from edx_notifications.data import NotificationMessage
...
@@ -26,7 +26,7 @@ from edx_notifications.data import NotificationMessage
log
=
logging
.
getLogger
(
__name__
)
log
=
logging
.
getLogger
(
__name__
)
@receiver
(
score_changed
)
@receiver
(
score_changed
,
dispatch_uid
=
"lms.courseware.score_changed"
)
def
on_score_changed
(
sender
,
**
kwargs
):
def
on_score_changed
(
sender
,
**
kwargs
):
"""
"""
Listens for a 'score_changed' signal and when observed
Listens for a 'score_changed' signal and when observed
...
...
lms/envs/aws.py
View file @
a8d1d663
...
@@ -590,6 +590,8 @@ if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
...
@@ -590,6 +590,8 @@ if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
'schedule'
:
datetime
.
timedelta
(
hours
=
ENV_TOKENS
.
get
(
'THIRD_PARTY_AUTH_SAML_FETCH_PERIOD_HOURS'
,
24
)),
'schedule'
:
datetime
.
timedelta
(
hours
=
ENV_TOKENS
.
get
(
'THIRD_PARTY_AUTH_SAML_FETCH_PERIOD_HOURS'
,
24
)),
}
}
SOCIAL_AUTH_RESPECT_X_FORWARDED_HEADERS
=
ENV_TOKENS
.
get
(
'SOCIAL_AUTH_RESPECT_X_FORWARDED_HEADERS'
)
# FAKE EMAIL DOMAIN setting is used to generate an email for an automatically provisioned account in case
# FAKE EMAIL DOMAIN setting is used to generate an email for an automatically provisioned account in case
# it is not provided by IdP (which should'nt normally be the case for providers with automatic provisioning)
# it is not provided by IdP (which should'nt normally be the case for providers with automatic provisioning)
FAKE_EMAIL_DOMAIN
=
ENV_TOKENS
.
get
(
'FAKE_EMAIL_DOMAIN'
,
'fake-email-domain.foo'
)
FAKE_EMAIL_DOMAIN
=
ENV_TOKENS
.
get
(
'FAKE_EMAIL_DOMAIN'
,
'fake-email-domain.foo'
)
...
@@ -598,6 +600,11 @@ if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
...
@@ -598,6 +600,11 @@ if FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
# IdP provided name is empty, missing or does not pass minimal length check
# IdP provided name is empty, missing or does not pass minimal length check
THIRD_PARTY_AUTH_FALLBACK_FULL_NAME
=
ENV_TOKENS
.
get
(
'THIRD_PARTY_AUTH_FALLBACK_FULL_NAME'
,
"Unknown"
)
THIRD_PARTY_AUTH_FALLBACK_FULL_NAME
=
ENV_TOKENS
.
get
(
'THIRD_PARTY_AUTH_FALLBACK_FULL_NAME'
,
"Unknown"
)
# The following can be used to integrate a custom login form with third_party_auth.
# It should be a dict where the key is a word passed via ?auth_entry=, and the value is a
# dict with an arbitrary 'secret_key' and a 'url'.
THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS
=
AUTH_TOKENS
.
get
(
'THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS'
,
{})
##### OAUTH2 Provider ##############
##### OAUTH2 Provider ##############
if
FEATURES
.
get
(
'ENABLE_OAUTH2_PROVIDER'
):
if
FEATURES
.
get
(
'ENABLE_OAUTH2_PROVIDER'
):
OAUTH_OIDC_ISSUER
=
ENV_TOKENS
[
'OAUTH_OIDC_ISSUER'
]
OAUTH_OIDC_ISSUER
=
ENV_TOKENS
[
'OAUTH_OIDC_ISSUER'
]
...
...
lms/envs/common.py
View file @
a8d1d663
...
@@ -2692,6 +2692,7 @@ NOTIFICATION_CLICK_LINK_URL_MAPS = {
...
@@ -2692,6 +2692,7 @@ NOTIFICATION_CLICK_LINK_URL_MAPS = {
'open-edx.lms.leaderboard.*'
:
'/courses/{course_id}/cohort'
,
'open-edx.lms.leaderboard.*'
:
'/courses/{course_id}/cohort'
,
'open-edx.lms.discussions.*'
:
'/courses/{course_id}/discussion/{commentable_id}/threads/{thread_id}'
,
'open-edx.lms.discussions.*'
:
'/courses/{course_id}/discussion/{commentable_id}/threads/{thread_id}'
,
'open-edx.xblock.group-project.*'
:
'/courses/{course_id}/group_work?seqid={activity_location}'
,
'open-edx.xblock.group-project.*'
:
'/courses/{course_id}/group_work?seqid={activity_location}'
,
'open-edx.xblock.group-project-v2.*'
:
'/courses/{course_id}/group_work?activate_block_id={location}'
,
}
}
# list all known channel providers
# list all known channel providers
...
...
lms/envs/test.py
View file @
a8d1d663
...
@@ -261,6 +261,14 @@ AUTHENTICATION_BACKENDS = (
...
@@ -261,6 +261,14 @@ AUTHENTICATION_BACKENDS = (
FAKE_EMAIL_DOMAIN
=
'fake-email-domain.foo'
FAKE_EMAIL_DOMAIN
=
'fake-email-domain.foo'
THIRD_PARTY_AUTH_FALLBACK_FULL_NAME
=
"Unknown"
THIRD_PARTY_AUTH_FALLBACK_FULL_NAME
=
"Unknown"
THIRD_PARTY_AUTH_CUSTOM_AUTH_FORMS
=
{
'custom1'
:
{
'secret_key'
:
'opensesame'
,
'url'
:
'/misc/my-custom-registration-form'
,
'error_url'
:
'/misc/my-custom-sso-error-page'
},
}
################################## OPENID #####################################
################################## OPENID #####################################
FEATURES
[
'AUTH_USE_OPENID'
]
=
True
FEATURES
[
'AUTH_USE_OPENID'
]
=
True
FEATURES
[
'AUTH_USE_OPENID_PROVIDER'
]
=
True
FEATURES
[
'AUTH_USE_OPENID_PROVIDER'
]
=
True
...
...
lms/urls.py
View file @
a8d1d663
...
@@ -652,6 +652,7 @@ if settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING'):
...
@@ -652,6 +652,7 @@ if settings.FEATURES.get('AUTOMATIC_AUTH_FOR_TESTING'):
if
settings
.
FEATURES
.
get
(
'ENABLE_THIRD_PARTY_AUTH'
):
if
settings
.
FEATURES
.
get
(
'ENABLE_THIRD_PARTY_AUTH'
):
urlpatterns
+=
(
urlpatterns
+=
(
url
(
r''
,
include
(
'third_party_auth.urls'
)),
url
(
r''
,
include
(
'third_party_auth.urls'
)),
url
(
r'api/third_party_auth/'
,
include
(
'third_party_auth.api.urls'
)),
# NOTE: The following login_oauth_token endpoint is DEPRECATED.
# NOTE: The following login_oauth_token endpoint is DEPRECATED.
# Please use the exchange_access_token endpoint instead.
# Please use the exchange_access_token endpoint instead.
url
(
r'^login_oauth_token/(?P<backend>[^/]+)/$'
,
'student.views.login_oauth_token'
),
url
(
r'^login_oauth_token/(?P<backend>[^/]+)/$'
,
'student.views.login_oauth_token'
),
...
...
openedx/core/djangoapps/credit/signals.py
View file @
a8d1d663
...
@@ -30,7 +30,7 @@ def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=
...
@@ -30,7 +30,7 @@ def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=
log
.
info
(
u'Added task to update credit requirements for course "
%
s" to the task queue'
,
course_key
)
log
.
info
(
u'Added task to update credit requirements for course "
%
s" to the task queue'
,
course_key
)
@receiver
(
GRADES_UPDATED
)
@receiver
(
GRADES_UPDATED
,
dispatch_uid
=
"edxapp.credit.grades_updated"
)
def
listen_for_grade_calculation
(
sender
,
username
,
grade_summary
,
course_key
,
deadline
,
**
kwargs
):
# pylint: disable=unused-argument
def
listen_for_grade_calculation
(
sender
,
username
,
grade_summary
,
course_key
,
deadline
,
**
kwargs
):
# pylint: disable=unused-argument
"""Receive 'MIN_GRADE_REQUIREMENT_STATUS' signal and update minimum grade
"""Receive 'MIN_GRADE_REQUIREMENT_STATUS' signal and update minimum grade
requirement status.
requirement status.
...
...
requirements/edx/custom.txt
View file @
a8d1d663
# Custom requirements to be customized by individual OpenEdX instances
# Custom requirements to be customized by individual OpenEdX instances
-e git+https://github.com/edx/xblock-utils.git@25f15734ec8d29fde0e114bbd199fd48638865ae#egg=xblock-utils
# When updating a hash of an XBlock that uses xblock-utils, please update xblock-utils version hash here and in
# github.txt as well. Deployments install custom.txt before github.txt and github.txt installs xblock-utils. This might
# lead to installing an outdated version of xblock-utils and causing regressions. A note in github.txt is added to
# keep xblock-utils version there in sync with this one.
-e git+https://github.com/edx/xblock-utils.git@3b58c757f06943072b170654d676e95b9adb37b0#egg=xblock-utils
-e git+https://github.com/edx-solutions/xblock-mentoring.git@bd0b3f413ae7e8274985555adfd7de7af3eca84c#egg=xblock-mentoring
-e git+https://github.com/edx-solutions/xblock-mentoring.git@bd0b3f413ae7e8274985555adfd7de7af3eca84c#egg=xblock-mentoring
-e git+https://github.com/edx-solutions/xblock-image-explorer.git@21b9bcc4f2c7917463ab18a596161ac6c58c9c4a#egg=xblock-image-explorer
-e git+https://github.com/edx-solutions/xblock-image-explorer.git@21b9bcc4f2c7917463ab18a596161ac6c58c9c4a#egg=xblock-image-explorer
-e git+https://github.com/edx-solutions/xblock-drag-and-drop.git@92ee2055a16899090a073e1df81e35d5293ad767#egg=xblock-drag-and-drop
-e git+https://github.com/edx-solutions/xblock-drag-and-drop.git@92ee2055a16899090a073e1df81e35d5293ad767#egg=xblock-drag-and-drop
-e git+https://github.com/edx-solutions/xblock-drag-and-drop-v2.git@
5736ed8774b92c8b8396b5bd455f8a8fb80295fb
#egg=xblock-drag-and-drop-v2
-e git+https://github.com/edx-solutions/xblock-drag-and-drop-v2.git@
8dbe34cfb33ff72252ec66051108a2d2e757a498
#egg=xblock-drag-and-drop-v2
-e git+https://github.com/edx-solutions/xblock-ooyala.git@42f769d422850df81bcbd2dbcc344f86b6a17d8e#egg=xblock-ooyala
-e git+https://github.com/edx-solutions/xblock-ooyala.git@42f769d422850df81bcbd2dbcc344f86b6a17d8e#egg=xblock-ooyala
-e git+https://github.com/edx-solutions/xblock-group-project.git@6b3393a1a5eb76224ecd3311e870ab8adf4badbf#egg=xblock-group-project
-e git+https://github.com/edx-solutions/xblock-group-project.git@6b3393a1a5eb76224ecd3311e870ab8adf4badbf#egg=xblock-group-project
-e git+https://github.com/edx-solutions/xblock-adventure.git@effa22006bb6528bc6d3788787466eb4e74e1161#egg=xblock-adventure
-e git+https://github.com/edx-solutions/xblock-adventure.git@effa22006bb6528bc6d3788787466eb4e74e1161#egg=xblock-adventure
-e git+https://github.com/mckinseyacademy/xblock-poll.git@ca0e6eb4ef10c128d573c3cec015dcfee7984730#egg=xblock-poll
-e git+https://github.com/mckinseyacademy/xblock-poll.git@ca0e6eb4ef10c128d573c3cec015dcfee7984730#egg=xblock-poll
-e git+https://github.com/edx/edx-notifications.git@
8038452f6fbb2b95ad46f8fe7a2f80b145b45b9
c#egg=edx-notifications
-e git+https://github.com/edx/edx-notifications.git@
275b8354593048ecae3e06642985b702b81140c
c#egg=edx-notifications
-e git+https://github.com/open-craft/problem-builder.git@
cd2304e1add8a1a1c7d0eec08a27550e753ca9ae
#egg=problem-builder
-e git+https://github.com/open-craft/problem-builder.git@
fa5d5e59133b2fd95ebea1aabcaa36578775eb21
#egg=problem-builder
-e git+https://github.com/open-craft/xblock-group-project-v2.git@
efa6a82c50ee8e78737b2488cbf0a77efe499c00
#egg=xblock-group-project-v2
-e git+https://github.com/open-craft/xblock-group-project-v2.git@
648c357c2b57fe6fa5ff68a0c29e6e72f309b9ca
#egg=xblock-group-project-v2
-e git+https://github.com/OfficeDev/xblock-officemix.git@86238f5968a08db005717dbddc346808f1ed3716#egg=xblock-officemix
-e git+https://github.com/OfficeDev/xblock-officemix.git@86238f5968a08db005717dbddc346808f1ed3716#egg=xblock-officemix
-e git+https://github.com/edx-solutions/xblock.git@80d11e883cb0f4b554e1e566294cb7de383cffed#egg=xblock
-e git+https://github.com/edx-solutions/xblock.git@80d11e883cb0f4b554e1e566294cb7de383cffed#egg=xblock
requirements/edx/github.txt
View file @
a8d1d663
...
@@ -50,7 +50,8 @@ git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3
...
@@ -50,7 +50,8 @@ git+https://github.com/edx/ease.git@release-2015-07-14#egg=ease==0.1.3
-e git+https://github.com/edx/edx-search.git@release-2015-07-03#egg=edx-search
-e git+https://github.com/edx/edx-search.git@release-2015-07-03#egg=edx-search
-e git+https://github.com/edx/edx-milestones.git@release-2015-06-17#egg=edx-milestones
-e git+https://github.com/edx/edx-milestones.git@release-2015-06-17#egg=edx-milestones
git+https://github.com/edx/edx-lint.git@ed8c8d2a0267d4d42f43642d193e25f8bd575d9b#egg=edx_lint==0.2.3
git+https://github.com/edx/edx-lint.git@ed8c8d2a0267d4d42f43642d193e25f8bd575d9b#egg=edx_lint==0.2.3
-e git+https://github.com/edx/xblock-utils.git@213a97a50276d6a2504d8133650b2930ead357a0#egg=xblock-utils
# Note for the next rebase: custom.txt or one of XBlocks installed there might require a newer version of xblock-utils - please check versions
-e git+https://github.com/edx/xblock-utils.git@3b58c757f06943072b170654d676e95b9adb37b0#egg=xblock-utils
-e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive
-e git+https://github.com/edx-solutions/xblock-google-drive.git@138e6fa0bf3a2013e904a085b9fed77dab7f3f21#egg=xblock-google-drive
-e git+https://github.com/edx/edx-reverification-block.git@a286e89c73e1b788e35ac5b08a54b71a9fa63cfd#egg=edx-reverification-block
-e git+https://github.com/edx/edx-reverification-block.git@a286e89c73e1b788e35ac5b08a54b71a9fa63cfd#egg=edx-reverification-block
git+https://github.com/edx/ecommerce-api-client.git@1.0.0#egg=ecommerce-api-client==1.0.0
git+https://github.com/edx/ecommerce-api-client.git@1.0.0#egg=ecommerce-api-client==1.0.0
...
...
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