diff --git a/.travis.yml b/.travis.yml index ab272e5..493b877 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,11 +3,12 @@ language: python python: - "2.7" env: - - CKANVERSION=2.6.3 POSTGISVERSION=2 + - CKANVERSION=2.6.3 POSTGISVERSION=2 INTEGRATION_TEST=true - CKANVERSION=2.7.2 POSTGISVERSION=2 - CKANVERSION=2.7.3 POSTGISVERSION=2 INTEGRATION_TEST=true - - CKANVERSION=2.8.0 POSTGISVERSION=2 + - CKANVERSION=2.8.0 POSTGISVERSION=2 INTEGRATION_TEST=true services: + - docker - redis-server - postgresql addons: @@ -18,7 +19,7 @@ before_install: - tar -xzf geckodriver-v0.20.1-linux64.tar.gz -C geckodriver - export PATH=$PATH:$PWD/geckodriver install: - - bash bin/travis-build.bash + - . bin/travis-build.bash script: - bash bin/travis-run.sh after_success: coveralls diff --git a/README.md b/README.md index 3702911..ab463a9 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ OAuth2 CKAN extension The OAuth2 extension allows site visitors to login through an OAuth2 server. -**Note**: This extension is being tested in CKAN 2.6 and 2.7. These are therefore considered as the supported versions +**Note**: This extension is being tested in CKAN 2.6, 2.7 and 2.8. These are therefore considered as the supported versions ## Links diff --git a/bin/travis-build.bash b/bin/travis-build.bash index 5e77e12..b6d5dda 100755 --- a/bin/travis-build.bash +++ b/bin/travis-build.bash @@ -44,4 +44,21 @@ cd - echo "Installing ckanext-oauth2 and its requirements..." python setup.py develop -echo "travis-build.bash is done." \ No newline at end of file +if [ "$INTEGRATION_TEST" = "true" ]; then + sudo sh -c 'echo "\n[ SAN ]\nsubjectAltName=DNS:localhost" >> /etc/ssl/openssl.cnf' + sudo openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 \ + -subj '/O=API Umbrella/CN=localhost' \ + -keyout /etc/ssl/self_signed.key -out /usr/local/share/ca-certificates/self_signed.crt \ + -reqexts SAN -extensions SAN + + sudo update-ca-certificates + export REQUESTS_CA_BUNDLE="/etc/ssl/certs/ca-certificates.crt" + docker network create main + docker run -d --network main -e MYSQL_ROOT_PASSWORD=idm -e MYSQL_ROOT_HOST=% --name mysql mysql/mysql-server:5.7.21 + docker run -d -p 443:443 --network main -e DATABASE_HOST=mysql -v "${TRAVIS_BUILD_DIR}/ci/idm-config.js:/opt/fiware-idm/config.js:ro" -v /etc/ssl/self_signed.key:/opt/fiware-idm/certs/self_signed.key:ro -v /usr/local/share/ca-certificates/self_signed.crt:/opt/fiware-idm/certs/self_signed.crt:ro --name idm fiware/idm + + # Wait until idm is ready + sleep 30 +fi + +echo "travis-build.bash is done." diff --git a/ci/idm-config.js b/ci/idm-config.js new file mode 100644 index 0000000..7291c53 --- /dev/null +++ b/ci/idm-config.js @@ -0,0 +1,89 @@ +var config = {}; + +config.host = 'https://localhost'; +config.port = 3000 + +// HTTPS enable +config.https = { + enabled: true, + cert_file: 'certs/self_signed.crt', + key_file: 'certs/self_signed.key', + port: 443 +}; + +// Config email list type to use domain filtering +config.email_list_type = null // whitelist or blacklist + +// Secret for user sessions in web +config.session = { + secret: 'nodejs_idm', // Must be changed + expires: 60 * 60 * 1000 // 1 hour +} + +// Key to encrypt user passwords +config.password_encryption = { + key: 'nodejs_idm' // Must be changed +} + +// Config oauth2 parameters +config.oauth2 = { + authorization_code_lifetime: 5 * 60, // Five minutes + access_token_lifetime: 60 * 60, // One hour + refresh_token_lifetime: 60 * 60 * 24 * 14 // Two weeks +} + +// Config api parameters +config.api = { + token_lifetime: 60 * 60 // One hour +} + +// Enable authzforce +config.authzforce = { + enabled: false, + host: '', + port: 8080 +} + +var database_host = (process.env.DATABASE_HOST) ? process.env.DATABASE_HOST : 'localhost' + +// Database info +config.database = { + host: database_host, // default: 'localhost' + password: 'idm', // default: 'idm' + username: 'root', // default: 'root' + database: 'idm', // default: 'idm' + dialect: 'mysql', // default: 'mysql' + port: undefined // default: undefined (which means that the port + // is the default for each dialect) +}; + +// External user authentication +config.external_auth = { + enabled: false, + authentication_driver: 'custom_authentication_driver', + database: { + host: 'localhost', + database: 'db_name', + username: 'db_user', + password: 'db_pass', + user_table: 'user', + dialect: 'mysql', + port: undefined + } +} + +// Email configuration +config.mail = { + host: 'localhost', + port: 25, + from: 'noreply@localhost' +} + + +// Config themes +config.site = { + title: 'Identity Manager', + theme: 'default' +}; + +module.exports = config; diff --git a/ckanext/oauth2/controller.py b/ckanext/oauth2/controller.py index 4c1fab8..acd965b 100644 --- a/ckanext/oauth2/controller.py +++ b/ckanext/oauth2/controller.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- # Copyright (c) 2014 CoNWeT Lab., Universidad Politécnica de Madrid +# Copyright (c) 2018 Future Internet Consulting and Development Solutions S.L. # This file is part of OAuth2 CKAN Extension. @@ -17,15 +18,18 @@ # You should have received a copy of the GNU Affero General Public License # along with OAuth2 CKAN Extension. If not, see . +from __future__ import unicode_literals + import logging import constants -import oauth2 +from ckan.common import session import ckan.lib.helpers as helpers import ckan.lib.base as base +import ckan.plugins.toolkit as toolkit +import oauth2 -from ckan.common import session -from ckanext.oauth2.plugin import toolkit +from ckanext.oauth2.plugin import _get_previous_page log = logging.getLogger(__name__) @@ -36,6 +40,19 @@ class OAuth2Controller(base.BaseController): def __init__(self): self.oauth2helper = oauth2.OAuth2Helper() + def login(self): + log.debug('login') + + # Log in attemps are fired when the user is not logged in and they click + # on the log in button + + # Get the page where the user was when the loggin attemp was fired + # When the user is not logged in, he/she should be redirected to the dashboard when + # the system cannot get the previous page + came_from_url = _get_previous_page(constants.INITIAL_PAGE) + + self.oauth2helper.challenge(came_from_url) + def callback(self): try: token = self.oauth2helper.get_token() diff --git a/ckanext/oauth2/oauth2.py b/ckanext/oauth2/oauth2.py index cf0bf91..81ae95c 100644 --- a/ckanext/oauth2/oauth2.py +++ b/ckanext/oauth2/oauth2.py @@ -23,7 +23,6 @@ import base64 import ckan.model as model -import constants import db import json import logging @@ -37,6 +36,9 @@ from requests_oauthlib import OAuth2Session import six +import constants + + log = logging.getLogger(__name__) @@ -48,11 +50,16 @@ def get_came_from(state): return json.loads(b64decode(state)).get(constants.CAME_FROM_FIELD, '/') +REQUIRED_CONF = ("authorization_endpoint", "token_endpoint", "client_id", "client_secret", "profile_api_url", "profile_api_user_field", "profile_api_mail_field") + + class OAuth2Helper(object): def __init__(self): self.verify_https = os.environ.get('OAUTHLIB_INSECURE_TRANSPORT', '') == "" + if self.verify_https and os.environ.get("REQUESTS_CA_BUNDLE", "").strip() != "": + self.verify_https = os.environ["REQUESTS_CA_BUNDLE"].strip() self.legacy_idm = six.text_type(os.environ.get('CKAN_OAUTH2_LEGACY_IDM', toolkit.config.get('ckan.oauth2.legacy_idm', ''))).strip().lower() in ("true", "1", "on") self.authorization_endpoint = six.text_type(os.environ.get('CKAN_OAUTH2_AUTHORIZATION_ENDPOINT', toolkit.config.get('ckan.oauth2.authorization_endpoint', ''))).strip() @@ -61,7 +68,7 @@ def __init__(self): self.client_id = six.text_type(os.environ.get('CKAN_OAUTH2_CLIENT_ID', toolkit.config.get('ckan.oauth2.client_id', ''))).strip() self.client_secret = six.text_type(os.environ.get('CKAN_OAUTH2_CLIENT_SECRET', toolkit.config.get('ckan.oauth2.client_secret', ''))).strip() self.scope = six.text_type(os.environ.get('CKAN_OAUTH2_SCOPE', toolkit.config.get('ckan.oauth2.scope', ''))).strip() - self.rememberer_name = six.text_type(os.environ.get('CKAN_OAUTH2_REMEMBER_NAME', toolkit.config.get('ckan.oauth2.rememberer_name', ''))).strip() + self.rememberer_name = six.text_type(os.environ.get('CKAN_OAUTH2_REMEMBER_NAME', toolkit.config.get('ckan.oauth2.rememberer_name', 'auth_tkt'))).strip() self.profile_api_user_field = six.text_type(os.environ.get('CKAN_OAUTH2_PROFILE_API_USER_FIELD', toolkit.config.get('ckan.oauth2.profile_api_user_field', ''))).strip() self.profile_api_fullname_field = six.text_type(os.environ.get('CKAN_OAUTH2_PROFILE_API_FULLNAME_FIELD', toolkit.config.get('ckan.oauth2.profile_api_fullname_field', ''))).strip() self.profile_api_mail_field = six.text_type(os.environ.get('CKAN_OAUTH2_PROFILE_API_MAIL_FIELD', toolkit.config.get('ckan.oauth2.profile_api_mail_field', ''))).strip() @@ -85,9 +92,9 @@ def challenge(self, came_from_url): state = generate_state(came_from_url) oauth = OAuth2Session(self.client_id, redirect_uri=self.redirect_uri, scope=self.scope, state=state) auth_url, _ = oauth.authorization_url(self.authorization_endpoint) - toolkit.response.status = 302 - toolkit.response.location = auth_url log.debug('Challenge: Redirecting challenge to page {0}'.format(auth_url)) + # CKAN 2.6 only supports bytes + return toolkit.redirect_to(auth_url.encode('utf-8')) def get_token(self): oauth = OAuth2Session(self.client_id, redirect_uri=self.redirect_uri, scope=self.scope) diff --git a/ckanext/oauth2/plugin.py b/ckanext/oauth2/plugin.py index 3c7fa32..3d2e1db 100644 --- a/ckanext/oauth2/plugin.py +++ b/ckanext/oauth2/plugin.py @@ -20,13 +20,13 @@ from __future__ import unicode_literals -import constants import logging import oauth2 import os from functools import partial from ckan import plugins +from ckan.common import g from ckan.plugins import toolkit from urlparse import urlparse @@ -62,6 +62,27 @@ def request_reset(context, data_dict): return _no_permissions(context, msg) +def _get_previous_page(default_page): + if 'came_from' not in toolkit.request.params: + came_from_url = toolkit.request.headers.get('Referer', default_page) + else: + came_from_url = toolkit.request.params.get('came_from', default_page) + + came_from_url_parsed = urlparse(came_from_url) + + # Avoid redirecting users to external hosts + if came_from_url_parsed.netloc != '' and came_from_url_parsed.netloc != toolkit.request.host: + came_from_url = default_page + + # When a user is being logged and REFERER == HOME or LOGOUT_PAGE + # he/she must be redirected to the dashboard + pages = ['/', '/user/logged_out_redirect'] + if came_from_url_parsed.path in pages: + came_from_url = default_page + + return came_from_url + + class OAuth2Plugin(plugins.SingletonPlugin): plugins.implements(plugins.IAuthenticator, inherit=True) @@ -78,6 +99,10 @@ def __init__(self, name=None): def before_map(self, m): log.debug('Setting up the redirections to the OAuth2 service') + m.connect('/user/login', + controller='ckanext.oauth2.controller:OAuth2Controller', + action='login') + # We need to handle petitions received to the Callback URL # since some error can arise and we need to process them m.connect('/oauth2/callback', @@ -125,70 +150,14 @@ def _refresh_and_save_token(user_name): # If we have been able to log in the user (via API or Session) if user_name: + g.user = user_name toolkit.c.user = user_name toolkit.c.usertoken = self.oauth2helper.get_stored_token(user_name) toolkit.c.usertoken_refresh = partial(_refresh_and_save_token, user_name) else: + g.user = None log.warn('The user is not currently logged...') - def _get_previous_page(self, default_page): - if 'came_from' not in toolkit.request.params: - came_from_url = toolkit.request.headers.get('Referer', default_page) - else: - came_from_url = toolkit.request.params.get('came_from', default_page) - - came_from_url_parsed = urlparse(came_from_url) - - # Avoid redirecting users to external hosts - if came_from_url_parsed.netloc != '' and came_from_url_parsed.netloc != toolkit.request.host: - came_from_url = default_page - - # When a user is being logged and REFERER == HOME or LOGOUT_PAGE - # he/she must be redirected to the dashboard - pages = ['/', '/user/logged_out_redirect'] - if came_from_url_parsed.path in pages: - came_from_url = default_page - - return came_from_url - - def login(self): - log.debug('login') - - # Log in attemps are fired when the user is not logged in and they click - # on the log in button - - # Get the page where the user was when the loggin attemp was fired - # When the user is not logged in, he/she should be redirected to the dashboard when - # the system cannot get the previous page - came_from_url = self._get_previous_page(constants.INITIAL_PAGE) - - self.oauth2helper.challenge(came_from_url) - - def abort(self, status_code, detail, headers, comment): - log.debug('abort') - - # If the user is authenticated, but they cannot access a protected resource, the system - # should redirect them to the previous page. If the user is not redirected, the system - # will try to reauthenticate the user generating a redirect loop: - # (authenticate -> user not allowed -> auto log out -> authenticate -> ...) - # If the user is not authenticated, the system should start the authentication process - - if toolkit.c.user: # USER IS AUTHENTICATED - # When the user is logged in, he/she should be redirected to the main page when - # the system cannot get the previous page - came_from_url = self._get_previous_page('/') - - # Init headers and set Location - if headers is None: - headers = {} - headers['Location'] = came_from_url - - # 302 -> Found - return 302, detail, headers, comment - else: # USER IS NOT AUTHENTICATED - # By not modifying the received parameters, the authentication process will start - return status_code, detail, headers, comment - def get_auth_functions(self): # we need to prevent some actions being authorized. return { diff --git a/ckanext/oauth2/tests/test_controller.py b/ckanext/oauth2/tests/test_controller.py index 689268c..b4c7b8a 100644 --- a/ckanext/oauth2/tests/test_controller.py +++ b/ckanext/oauth2/tests/test_controller.py @@ -18,14 +18,16 @@ # You should have received a copy of the GNU Affero General Public License # along with OAuth2 CKAN Extension. If not, see . +from base64 import b64decode, b64encode import unittest -import ckanext.oauth2.controller as controller import json -from base64 import b64decode, b64encode from mock import MagicMock from parameterized import parameterized +from ckanext.oauth2 import controller, plugin + + RETURNED_STATUS = 302 EXAMPLE_FLASH = 'This is a test' EXCEPTION_MSG = 'Invalid' @@ -55,8 +57,9 @@ def setUp(self): self._oauth2 = controller.oauth2 controller.oauth2 = MagicMock() - self._toolkit = controller.toolkit - controller.toolkit = MagicMock() + self._toolkit_controller = controller.toolkit + self._toolkit_plugin = plugin.toolkit + plugin.toolkit = controller.toolkit = MagicMock() self.__session = controller.session controller.session = MagicMock() @@ -67,8 +70,9 @@ def tearDown(self): # Unmock the function controller.helpers = self._helpers controller.oauth2 = self._oauth2 - controller.toolkit = self._toolkit + controller.toolkit = self._toolkit_controller controller.session = self.__session + plugin.toolkit = self._toolkit_plugin def generate_state(self, url): return b64encode(bytes(json.dumps({CAME_FROM_FIELD: url}))) @@ -76,7 +80,7 @@ def generate_state(self, url): def get_came_from(self, state): return json.loads(b64decode(state)).get(CAME_FROM_FIELD, '/') - def test_controller_no_errors(self): + def test_callback_no_errors(self): oauth2Helper = controller.oauth2.OAuth2Helper.return_value token = 'TOKEN' @@ -104,7 +108,7 @@ def test_controller_no_errors(self): ('/', VoidException(), None, type(VoidException()).__name__), ('/about', Exception(EXCEPTION_MSG), EXAMPLE_FLASH, EXAMPLE_FLASH) ]) - def test_controller_errors(self, came_from=None, exception=Exception(EXCEPTION_MSG), + def test_callback_errors(self, came_from=None, exception=Exception(EXCEPTION_MSG), error_description=None, expected_flash=EXCEPTION_MSG): # Recover function @@ -127,3 +131,33 @@ def test_controller_errors(self, came_from=None, exception=Exception(EXCEPTION_M self.assertEquals(RETURNED_STATUS, controller.toolkit.response.status_int) self.assertEquals(came_from, controller.toolkit.response.location) controller.helpers.flash_error.assert_called_once_with(expected_flash) + + @parameterized.expand([ + (), + (None, None, '/dashboard'), + ('/about', None, '/about'), + ('/about', '/ckan-admin', '/ckan-admin'), + (None, '/ckan-admin', '/ckan-admin'), + ('/', None, '/dashboard'), + ('/user/logged_out_redirect', None, '/dashboard'), + ('/', '/ckan-admin', '/ckan-admin'), + ('/user/logged_out_redirect', '/ckan-admin', '/ckan-admin'), + ('http://google.es', None, '/dashboard'), + ('http://google.es', None, '/dashboard') + ]) + def test_login(self, referer=None, came_from=None, expected_referer='/dashboard'): + + # The login function will check these variables + controller.toolkit.request.headers = {} + controller.toolkit.request.params = {} + + if referer: + controller.toolkit.request.headers['Referer'] = referer + + if came_from: + controller.toolkit.request.params['came_from'] = came_from + + # Call the function + self.controller.login() + + self.controller.oauth2helper.challenge.assert_called_once_with(expected_referer) diff --git a/ckanext/oauth2/tests/test_oauth2.py b/ckanext/oauth2/tests/test_oauth2.py index 0f5c896..931f69e 100644 --- a/ckanext/oauth2/tests/test_oauth2.py +++ b/ckanext/oauth2/tests/test_oauth2.py @@ -262,7 +262,6 @@ def test_challenge(self): came_from = '/came_from_example' oauth2.toolkit.request = request - oauth2.toolkit.response = MagicMock() # Call the method helper.challenge(came_from) @@ -271,8 +270,7 @@ def test_challenge(self): state = urlencode({'state': b64encode(bytes(json.dumps({'came_from': came_from})))}) expected_url = 'https://test/oauth2/authorize/?response_type=code&client_id=client-id&' + \ 'redirect_uri=http%3A%2F%2Flocalhost%3A5000%2Foauth2%2Fcallback&' + state - self.assertEquals(302, oauth2.toolkit.response.status) - self.assertEquals(expected_url, oauth2.toolkit.response.location) + oauth2.toolkit.redirect_to.assert_called_once_with(expected_url) @parameterized.expand([ ('test_user', 'Test User Full Name', 'test@test.com'), @@ -492,7 +490,7 @@ def test_update_token(self, user_exists): (True,), (False,) ]) - @patch.dict(os.environ, {'OAUTHLIB_INSECURE_TRANSPORT': ''}) + @patch.dict(os.environ, {'OAUTHLIB_INSECURE_TRANSPORT': '', 'REQUESTS_CA_BUNDLE': ''}) def test_refresh_token(self, user_exists): username = 'user' helper = self.helper = self._helper() diff --git a/ckanext/oauth2/tests/test_plugin.py b/ckanext/oauth2/tests/test_plugin.py index fb4ab7b..6a6abd1 100644 --- a/ckanext/oauth2/tests/test_plugin.py +++ b/ckanext/oauth2/tests/test_plugin.py @@ -21,7 +21,7 @@ import unittest import ckanext.oauth2.plugin as plugin -from mock import MagicMock +from mock import MagicMock, patch from parameterized import parameterized AUTHORIZATION_HEADER = 'custom_header' @@ -110,7 +110,7 @@ def test_auth_functions(self): self.assertEquals(False, function_result['success']) @parameterized.expand([ - (), + ({}, None, None, None), ({}, None, 'test', 'test'), ({AUTHORIZATION_HEADER: 'api_key'}, 'test', None, 'test'), ({AUTHORIZATION_HEADER: 'api_key'}, 'test', 'test2', 'test'), @@ -119,7 +119,8 @@ def test_auth_functions(self): ({'invalid_header': 'api_key'}, 'test', None, None), ({'invalid_header': 'api_key'}, 'test', 'test2', 'test2'), ]) - def test_identify(self, headers={}, authenticate_result=None, identity=None, expected_user=None): + @patch("ckanext.oauth2.plugin.g") + def test_identify(self, headers, authenticate_result, identity, expected_user, g_mock): self._set_identity(identity) @@ -163,6 +164,7 @@ def authenticate_side_effect(identity): else: self.assertEquals(0, self._plugin.oauth2helper.identify.call_count) + self.assertEquals(expected_user, g_mock.user) self.assertEquals(expected_user, plugin.toolkit.c.user) if expected_user is None: @@ -175,92 +177,3 @@ def authenticate_side_effect(identity): plugin.toolkit.c.usertoken_refresh() self._plugin.oauth2helper.refresh_token.assert_called_once_with(expected_user) self.assertEquals(newtoken, plugin.toolkit.c.usertoken) - - @parameterized.expand([ - (), - (None, None, '/dashboard'), - ('/about', None, '/about'), - ('/about', '/ckan-admin', '/ckan-admin'), - (None, '/ckan-admin', '/ckan-admin'), - ('/', None, '/dashboard'), - ('/user/logged_out_redirect', None, '/dashboard'), - ('/', '/ckan-admin', '/ckan-admin'), - ('/user/logged_out_redirect', '/ckan-admin', '/ckan-admin'), - ('http://google.es', None, '/dashboard'), - ('http://google.es', None, '/dashboard') - ]) - def test_login(self, referer=None, came_from=None, expected_referer='/dashboard'): - - # The login function will check these variables - plugin.toolkit.request.headers = {} - plugin.toolkit.request.params = {} - - self._plugin.oauth2helper.challenge = MagicMock() - - if referer: - plugin.toolkit.request.headers['Referer'] = referer - - if came_from: - plugin.toolkit.request.params['came_from'] = came_from - - # Call the function - self._plugin.login() - - self._plugin.oauth2helper.challenge.assert_called_once_with(expected_referer) - - @parameterized.expand([ - (), - ('user', None, None, None, '/'), - ('user', None, None, {'Param1': 'value1', 'paRam2': 'value2'}, '/'), - ('user', '/about', None, None, '/about'), - ('user', '/about', '/ckan-admin', None, '/ckan-admin'), - ('user', None, '/ckan-admin', None, '/ckan-admin'), - ('user', '/', None, None, '/'), - ('user', '/user/logged_out_redirect', None, None, '/'), - ('user', '/', '/ckan-admin', None, '/ckan-admin'), - ('user', '/user/logged_out_redirect', '/ckan-admin', None, '/ckan-admin'), - ('user', 'http://google.es', None, None, '/'), - ('user', 'http://google.es', None, None, '/'), - ('user', 'http://' + HOST + '/about', None, None, 'http://' + HOST + '/about'), - ('user', 'http://' + HOST + '/about', '/other_url', None, '/other_url'), - (None, '/about', '/other', None, None), - ]) - def test_abort(self, user='user', referer=None, came_from=None, headers=None, expected_location='/'): - - # The abort function will check these variables - plugin.toolkit.c.user = user - plugin.toolkit.request.host = HOST - plugin.toolkit.request.headers = {} - plugin.toolkit.request.params = {} - - if referer: - plugin.toolkit.request.headers['Referer'] = referer - - if came_from: - plugin.toolkit.request.params['came_from'] = came_from - - # Call the function - initial_status_code = 401 - initial_detail = 'DETAIL' - initial_headers = None if not headers else headers.copy() - initial_comment = 'COMMENT' - - # headers will be modified inside the function, but we should retain a copy (initial_headers) - status_code, detail, new_headers, comment = self._plugin.abort(initial_status_code, initial_detail, headers, initial_comment) - - # Verifications - self.assertEquals(initial_detail, detail) - self.assertEquals(initial_comment, comment) - - if user: - self.assertEquals(302, status_code) - self.assertEquals(new_headers['Location'], expected_location) - else: - self.assertEquals(initial_status_code, status_code) - self.assertEquals(initial_headers, new_headers) - - # Check previous headers if they were not None - if initial_headers: - for header in initial_headers: - self.assertIn(header, new_headers) - self.assertEquals(initial_headers[header], new_headers[header]) diff --git a/ckanext/oauth2/tests/test_selenium.py b/ckanext/oauth2/tests/test_selenium.py index 2aa80e6..0614de3 100644 --- a/ckanext/oauth2/tests/test_selenium.py +++ b/ckanext/oauth2/tests/test_selenium.py @@ -18,6 +18,8 @@ # You should have received a copy of the GNU Affero General Public License # along with OAuth2 CKAN Extension. If not, see . +from __future__ import print_function + import unittest import os from subprocess import Popen @@ -25,16 +27,20 @@ from urlparse import urljoin from parameterized import parameterized +import requests from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.support.ui import WebDriverWait -IDM_URL = "https://account.lab.fiware.org" -FILAB2_MAIL = "filab2@mailinator.com" -FILAB3_MAIL = "filab3@mailinator.com" -FILAB_PASSWORD = "filab1234" + +IDM_URL = "https://localhost" +FILAB2_MAIL = "admin@test.com" +FILAB_PASSWORD = "1234" PASS_INTEGRATION_TESTS = os.environ.get("INTEGRATION_TEST", "").strip().lower() in ('1', 'true', 'on') +AUTH_TOKEN_ENDPOINT = "v1/auth/tokens" +APPLICATION_ENDPOINT = "v1/applications" + @unittest.skipUnless(PASS_INTEGRATION_TESTS, "set INTEGRATION_TEST environment variable (e.g. INTEGRATION_TEST=true) for running the integration tests") class IntegrationTest(unittest.TestCase): @@ -45,23 +51,50 @@ def setUpClass(cls): if not PASS_INTEGRATION_TESTS: return + # Get an admin token + body = { + "name": "admin@test.com", + "password": "1234" + } + url = urljoin(IDM_URL, AUTH_TOKEN_ENDPOINT) + response = requests.post(url, json=body) + + token = response.headers["X-Subject-Token"] + + # Create the OAuth2 application + headers = { + "X-Auth-Token": token + } + + body = { + "application": { + "name": "Travis Selenium Tests", + "description": "Travis Selenium Tests", + "redirect_uri": "http://localhost:5000/oauth2/callback", + "url": "http://localhost:5000", + "grant_type": [ + "authorization_code" + ] + } + } + + url = urljoin(IDM_URL, APPLICATION_ENDPOINT) + response = requests.post(url, json=body, headers=headers) + app = response.json() + + # Run CKAN env = os.environ.copy() env['DEBUG'] = 'True' - env['OAUTHLIB_INSECURE_TRANSPORT'] = 'True' + env['OAUTHLIB_INSECURE_TRANSPORT'] = 'False' + env['CKAN_OAUTH2_CLIENT_ID'] = app['application']['id'] + env['CKAN_OAUTH2_CLIENT_SECRET'] = app['application']['secret'] cls._process = Popen(['paster', 'serve', 'test-fiware.ini'], env=env) + # Init Selenium cls.driver = webdriver.Firefox() cls.base_url = 'http://localhost:5000/' cls.driver.set_window_size(1024, 768) - cls.driver.get(IDM_URL) - cls._introduce_log_in_parameters() - cls.driver.get(urljoin(IDM_URL, '/idm/myApplications/361020fd7cf64456890dd98da88e64f3/edit/')) - id_callbackurl = WebDriverWait(cls.driver, 10).until(EC.presence_of_element_located((By.ID, "id_callbackurl"))) - id_callbackurl.clear() - id_callbackurl.send_keys(urljoin(cls.base_url, '/oauth2/callback')) - cls.driver.find_element_by_xpath("//button[@type='submit']").click() - @classmethod def tearDownClass(cls): # nose calls this method also if they are going to be skiped @@ -74,7 +107,7 @@ def tearDownClass(cls): @classmethod def _introduce_log_in_parameters(cls, username=FILAB2_MAIL, password=FILAB_PASSWORD): driver = cls.driver - id_username = WebDriverWait(cls.driver, 10).until(EC.presence_of_element_located((By.ID, "id_username"))) + id_username = WebDriverWait(cls.driver, 10).until(EC.presence_of_element_located((By.ID, "id_email"))) id_username.clear() id_username.send_keys(username) driver.find_element_by_id("id_password").clear() @@ -110,47 +143,45 @@ def test_basic_login(self): driver = self.driver self._log_in(self.base_url) WebDriverWait(driver, 20).until(lambda driver: (self.base_url + 'dashboard') == driver.current_url) - self.assertEqual("filab2 Example User", driver.find_element_by_link_text("filab2 Example User").text) + self.assertEqual("admin", driver.find_element_by_link_text("admin").text) driver.find_element_by_link_text("About").click() WebDriverWait(driver, 20).until(lambda driver: (self.base_url + 'about') == driver.current_url) - self.assertEqual("filab2 Example User", driver.find_element_by_css_selector("span.username").text) + self.assertEqual("admin", driver.find_element_by_css_selector("span.username").text) driver.find_element_by_css_selector("a[title=\"Edit settings\"]").click() time.sleep(3) # Wait the OAuth2 Server to return the page - assert driver.current_url.startswith(IDM_URL + "/settings") + self.assertTrue(driver.current_url.startswith(IDM_URL + "/idm/settings"), "%s does not starts with %s" % (driver.current_url, IDM_URL + "/idm/settings")) def test_basic_login_different_referer(self): driver = self.driver self._log_in(self.base_url + "about") WebDriverWait(driver, 20).until(lambda driver: (self.base_url + 'about') == driver.current_url) - self.assertEqual("filab2 Example User", driver.find_element_by_css_selector("span.username").text) + self.assertEqual("admin", driver.find_element_by_css_selector("span.username").text) driver.find_element_by_link_text("Datasets").click() WebDriverWait(driver, 20).until(lambda driver: (self.base_url + 'dataset') == driver.current_url) - self.assertEqual("filab2 Example User", driver.find_element_by_css_selector("span.username").text) + self.assertEqual("admin", driver.find_element_by_css_selector("span.username").text) def test_user_access_unauthorized_page(self): driver = self.driver self._log_in(self.base_url) driver.get(self.base_url + "ckan-admin") - # Check that the user has been redirected to the main page - WebDriverWait(driver, 10).until(lambda driver: driver.current_url == self.base_url) # Check that an error message is shown - assert driver.find_element_by_xpath("//div/div/div/div").text.startswith("Need to be system administrator to administer") + self.assertIn("Need to be system administrator to administer", self.driver.find_element_by_tag_name('body').text) def test_register_btn(self): driver = self.driver driver.get(self.base_url) WebDriverWait(driver, 10).until(EC.element_to_be_clickable((By.LINK_TEXT, "Register"))).click() - WebDriverWait(driver, 10).until(lambda driver: driver.current_url == (IDM_URL + "/sign_up/")) + WebDriverWait(driver, 10).until(lambda driver: driver.current_url == (IDM_URL + "/sign_up")) @parameterized.expand([ - ("user/register", IDM_URL + "/sign_up/"), - ("user/reset", IDM_URL + "/password/request/") + ("user/register", IDM_URL + "/sign_up"), + ("user/reset", IDM_URL + "/password/request") ]) def test_register(self, action, expected_url): driver = self.driver driver.get(self.base_url + action) - WebDriverWait(driver, 10).until(lambda driver: driver.current_url == expected_url) + WebDriverWait(driver, 10).until(lambda driver: print(driver.current_url) or driver.current_url == expected_url) if __name__ == "__main__": diff --git a/test-fiware.ini b/test-fiware.ini index 3507fbc..3755ef7 100644 --- a/test-fiware.ini +++ b/test-fiware.ini @@ -33,21 +33,21 @@ ckan.plugins = oauth2 ## OAuth2 configuration ckan.oauth2.logout_url = /user/logged_out -ckan.oauth2.register_url = https://account.lab.fiware.org/sign_up -ckan.oauth2.reset_url = https://account.lab.fiware.org/password/request -ckan.oauth2.edit_url = https://account.lab.fiware.org/settings -ckan.oauth2.authorization_endpoint = https://account.lab.fiware.org/oauth2/authorize -ckan.oauth2.token_endpoint = https://account.lab.fiware.org/oauth2/token -ckan.oauth2.profile_api_url = https://account.lab.fiware.org/user -ckan.oauth2.client_id = 361020fd7cf64456890dd98da88e64f3 -ckan.oauth2.client_secret = edf713bf8a2344139f46a757fadae24f +ckan.oauth2.register_url = https://localhost/sign_up +ckan.oauth2.reset_url = https://localhost/password/request +ckan.oauth2.edit_url = https://localhost/idm/settings +ckan.oauth2.authorization_endpoint = https://localhost/oauth2/authorize +ckan.oauth2.token_endpoint = https://localhost/oauth2/token +ckan.oauth2.profile_api_url = https://localhost/user +# The following parameters are configured through environment variables +#ckan.oauth2.client_id = 361020fd7cf64456890dd98da88e64f3 +#ckan.oauth2.client_secret = edf713bf8a2344139f46a757fadae24f ckan.oauth2.scope = all_info ckan.oauth2.rememberer_name = auth_tkt ckan.oauth2.profile_api_user_field = id ckan.oauth2.profile_api_fullname_field = displayName ckan.oauth2.profile_api_mail_field = email ckan.oauth2.authorization_header = X-Auth-Token -ckan.oauth2.legacy_idm = True #who.config_file = %(here)s/who-fiware.ini