Skip to content

Commit

Permalink
Merge pull request #19 from aarranz/feature/ckan-2.8
Browse files Browse the repository at this point in the history
Support CKAN 2.8
  • Loading branch information
aarranz authored Jul 6, 2018
2 parents 81ee0c9 + cf0382c commit ada0720
Show file tree
Hide file tree
Showing 12 changed files with 285 additions and 209 deletions.
7 changes: 4 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 18 additions & 1 deletion bin/travis-build.bash
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,21 @@ cd -
echo "Installing ckanext-oauth2 and its requirements..."
python setup.py develop

echo "travis-build.bash is done."
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."
89 changes: 89 additions & 0 deletions ci/idm-config.js
Original file line number Diff line number Diff line change
@@ -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;
23 changes: 20 additions & 3 deletions ckanext/oauth2/controller.py
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -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 <http://www.gnu.org/licenses/>.

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__)
Expand All @@ -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()
Expand Down
15 changes: 11 additions & 4 deletions ckanext/oauth2/oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

import base64
import ckan.model as model
import constants
import db
import json
import logging
Expand All @@ -37,6 +36,9 @@
from requests_oauthlib import OAuth2Session
import six

import constants


log = logging.getLogger(__name__)


Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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)
Expand Down
87 changes: 28 additions & 59 deletions ckanext/oauth2/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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)
Expand All @@ -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',
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit ada0720

Please sign in to comment.