Added a UI for confirming OAuth Access.
This will allow users to delegate permissions to a 3rd party service.
This commit is contained in:
48
common/test/acceptance/pages/lms/oauth2_confirmation.py
Normal file
48
common/test/acceptance/pages/lms/oauth2_confirmation.py
Normal file
@@ -0,0 +1,48 @@
|
||||
"""Pages relevant for OAuth2 confirmation."""
|
||||
from common.test.acceptance.pages.lms import BASE_URL
|
||||
|
||||
from bok_choy.page_object import PageObject
|
||||
|
||||
|
||||
class OAuth2Confirmation(PageObject):
|
||||
"""Page for OAuth2 confirmation view."""
|
||||
def __init__(self, browser, client_id="test-id", scopes=("email",)):
|
||||
super(OAuth2Confirmation, self).__init__(browser)
|
||||
self.client_id = client_id
|
||||
self.scopes = scopes
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
return "{}/oauth2/authorize?client_id={}&response_type=code&scope={}".format(
|
||||
BASE_URL, self.client_id, ' '.join(self.scopes))
|
||||
|
||||
def is_browser_on_page(self):
|
||||
return self.q(css="body.oauth2").visible
|
||||
|
||||
def cancel(self):
|
||||
"""
|
||||
Cancel the request.
|
||||
|
||||
This redirects to an invalid URI, because we don't want actual network
|
||||
connections being made.
|
||||
"""
|
||||
self.q(css="input[name=cancel]").click()
|
||||
|
||||
def confirm(self):
|
||||
"""
|
||||
Confirm OAuth access
|
||||
|
||||
This redirects to an invalid URI, because we don't want actual network
|
||||
connections being made.
|
||||
"""
|
||||
self.q(css="input[name=authorize]").click()
|
||||
|
||||
@property
|
||||
def has_error(self):
|
||||
"""Boolean for if the page has an error or not."""
|
||||
return self.q(css=".error").present
|
||||
|
||||
@property
|
||||
def error_message(self):
|
||||
"""Text of the page's error message."""
|
||||
return self.q(css='.error').text[0]
|
||||
60
common/test/acceptance/tests/lms/test_oauth2.py
Normal file
60
common/test/acceptance/tests/lms/test_oauth2.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Tests for OAuth2 permission delegation."""
|
||||
from common.test.acceptance.pages.lms.oauth2_confirmation import OAuth2Confirmation
|
||||
from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage
|
||||
from bok_choy.web_app_test import WebAppTest
|
||||
|
||||
from urlparse import urlparse, parse_qsl
|
||||
|
||||
|
||||
class OAuth2PermissionDelegationTests(WebAppTest):
|
||||
"""
|
||||
Tests for acceptance/denial of permission delegation requests.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(OAuth2PermissionDelegationTests, self).setUp()
|
||||
self.oauth_page = OAuth2Confirmation(self.browser)
|
||||
|
||||
def _auth(self):
|
||||
"""Authenticate the user."""
|
||||
AutoAuthPage(self.browser).visit()
|
||||
|
||||
def _qs(self, url):
|
||||
"""Parse url's querystring into a dict."""
|
||||
return dict(parse_qsl(urlparse(url).query))
|
||||
|
||||
def test_error_for_invalid_scopes(self):
|
||||
"""Requests for invalid scopes throw errors."""
|
||||
self._auth()
|
||||
self.oauth_page.scopes = ('email', 'does-not-exist')
|
||||
assert self.oauth_page.visit()
|
||||
|
||||
self.assertTrue(self.oauth_page.has_error)
|
||||
self.assertIn('not a valid scope', self.oauth_page.error_message)
|
||||
|
||||
def test_cancelling_redirects(self):
|
||||
"""
|
||||
If you cancel the request, you're redirected to the redirect_url with a
|
||||
denied query param.
|
||||
"""
|
||||
self._auth()
|
||||
assert self.oauth_page.visit()
|
||||
self.oauth_page.cancel()
|
||||
|
||||
# This redirects to an invalid URI.
|
||||
query = self._qs(self.browser.current_url)
|
||||
self.assertEqual('access_denied', query['error'])
|
||||
|
||||
def test_accepting_redirects(self):
|
||||
"""
|
||||
If you accept the request, you're redirected to the redirect_url with
|
||||
the correct query params.
|
||||
"""
|
||||
self._auth()
|
||||
assert self.oauth_page.visit()
|
||||
self.oauth_page.confirm()
|
||||
|
||||
# This redirects to an invalid URI.
|
||||
query = self._qs(self.browser.current_url)
|
||||
self.assertIn('code', query)
|
||||
15
common/test/db_fixtures/oauth.json
Normal file
15
common/test/db_fixtures/oauth.json
Normal file
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{
|
||||
"fields": {
|
||||
"client_id": "test-id",
|
||||
"client_secret": "test-secret",
|
||||
"client_type": 0,
|
||||
"name": "Test OAuth2 Client",
|
||||
"redirect_uri": "http://does-not-exist/",
|
||||
"url": "http://does-not-exist/",
|
||||
"user": null
|
||||
},
|
||||
"model": "oauth2.client",
|
||||
"pk": 3
|
||||
}
|
||||
]
|
||||
@@ -105,6 +105,9 @@ for log_name, log_level in LOG_OVERRIDES:
|
||||
# Enable milestones app
|
||||
FEATURES['MILESTONES_APP'] = True
|
||||
|
||||
# Enable oauth authentication, which we test.
|
||||
FEATURES['ENABLE_OAUTH2_PROVIDER'] = True
|
||||
|
||||
# Enable pre-requisite course
|
||||
FEATURES['ENABLE_PREREQUISITE_COURSES'] = True
|
||||
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
@import 'views/shoppingcart';
|
||||
@import 'views/homepage';
|
||||
@import 'views/support';
|
||||
@import 'views/oauth2';
|
||||
@import "views/financial-assistance";
|
||||
@import 'views/bookmarks';
|
||||
@import 'course/auto-cert';
|
||||
|
||||
16
lms/static/sass/views/_oauth2.scss
Normal file
16
lms/static/sass/views/_oauth2.scss
Normal file
@@ -0,0 +1,16 @@
|
||||
@import 'neat/neat'; // lib - Neat
|
||||
|
||||
.oauth2 {
|
||||
|
||||
@include outer-container();
|
||||
|
||||
.authorization-confirmation {
|
||||
|
||||
@include span-columns(6);
|
||||
@include shift(3);
|
||||
|
||||
line-height: 1.5em;
|
||||
padding: 50px 0;
|
||||
|
||||
}
|
||||
}
|
||||
59
lms/templates/provider/authorize.html
Normal file
59
lms/templates/provider/authorize.html
Normal file
@@ -0,0 +1,59 @@
|
||||
{% extends "main_django.html" %}
|
||||
|
||||
{% load scope i18n %}
|
||||
|
||||
{% block bodyclass %}oauth2{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="authorization-confirmation">
|
||||
{% if not error %}
|
||||
<p>
|
||||
{% blocktrans with application_name=client.name %}
|
||||
<strong>{{ application_name }}</strong> would like to access your data with the following permissions:
|
||||
{% endblocktrans %}
|
||||
</p>
|
||||
<ul>
|
||||
{% for permission in oauth_data.scope|scopes %}
|
||||
<li>
|
||||
{% if permission == "openid" %}
|
||||
{% trans "Read your user ID" %}
|
||||
{% elif permission == "profile" %}
|
||||
{% trans "Read your user profile" %}
|
||||
{% elif permission == "email" %}
|
||||
{% trans "Read your email address" %}
|
||||
{% elif permission == "course_staff" %}
|
||||
{% trans "Read the list of courses in which you are a staff member." %}
|
||||
{% elif permission == "course_instructor" %}
|
||||
{% trans "Read the list of courses in which you are an instructor." %}
|
||||
{% elif permission == "permissions" %}
|
||||
{% trans "To see if you are a global staff user" %}
|
||||
{% else %}
|
||||
{% blocktrans %}Manage your data: {{ permission }}{% endblocktrans %}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
<form method="post" action="{% url "oauth2:authorize" %}">
|
||||
{% csrf_token %}
|
||||
{{ form.errors }}
|
||||
{{ form.non_field_errors }}
|
||||
<fieldset>
|
||||
<div style="display: none;">
|
||||
<select type="select" name="scope" multiple="multiple">
|
||||
{% for scope in oauth_data.scope|scopes %}
|
||||
<option value="{{ scope }}" selected="selected">{{ scope }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<input type="submit" class="btn login large danger" name="cancel" value="Cancel" />
|
||||
<input type="submit" class="btn login large primary" name="authorize" value="Authorize" />
|
||||
</fieldset>
|
||||
</form>
|
||||
{% else %}
|
||||
<p class="error">
|
||||
{{ error }}
|
||||
{{ error_description }}
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user