Skip to content

Commit

Permalink
feat: create project
Browse files Browse the repository at this point in the history
  • Loading branch information
nmchau committed Dec 5, 2018
0 parents commit a45843c
Show file tree
Hide file tree
Showing 15 changed files with 1,218 additions and 0 deletions.
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
syntax: glob
*.pyc
*.egg-info
*.orig
.coverage
build
*.egg
.DS_Store
dist
development.ini
*.sw?
*~
node_modules
*.project
661 changes: 661 additions & 0 deletions LICENSE

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
include README.rst
include LICENSE
include requirements.txt
recursive-include ckanext/keycloak *.html *.json *.js *.less *.css *.mo
149 changes: 149 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
.. You should enable this project on travis-ci.org and coveralls.io to make
these badges work. The necessary Travis and Coverage config files have been
generated for you.
.. .. image:: https://travis-ci.org/etri-sodas/ckanext-keycloak.svg?branch=master
:target: https://travis-ci.org/etri-sodas/ckanext-keycloak
.. .. image:: https://coveralls.io/repos/etri-sodas/ckanext-keycloak/badge.svg
:target: https://coveralls.io/r/etri-sodas/ckanext-keycloak
.. .. image:: https://pypip.in/download/ckanext-keycloak/badge.svg
:target: https://pypi.python.org/pypi/etri-sodas/ckanext-keycloak/
:alt: Downloads
.. .. image:: https://pypip.in/version/ckanext-keycloak/badge.svg
:target: https://pypi.python.org/pypi/ckanext-keycloak/
:alt: Latest Version
.. .. image:: https://pypip.in/py_versions/ckanext-keycloak/badge.svg
:target: https://pypi.python.org/pypi/ckanext-keycloak/
:alt: Supported Python versions
.. .. image:: https://pypip.in/status/ckanext-keycloak/badge.svg
:target: https://pypi.python.org/pypi/ckanext-keycloak/
:alt: Development Status
.. .. image:: https://pypip.in/license/ckanext-keycloak/badge.svg
:target: https://pypi.python.org/pypi/ckanext-keycloak/
:alt: License
===========================================================
ckanext-keycloak - Keycloak authentication extension
===========================================================

.. Put a description of your extension here:
What does it do? What features does it have?
Consider including some screenshots or embedding a video!
Ckanext-keycloak is a extension for enalbing the user authentication with Keycloak, an open source software product to allow single sign-on with Identity Management and Access Management aimed at modern applications and services.

This extension provides an ability to let users use access-token from Keycloak server to access CKAN functions via CKAN REST Api.

Notes:

* A new user will be created automatically in ckan database for the corresponding keycloak user if it does not exist.
* Original ckan authentication still works normally with this extension.

------------
Requirements
------------

This extension was developed and tested under CKAN-2.7.3 and Keycloak-2.5.5

------------
Installation
------------

.. Add any additional install steps to the list below.
For example installing any non-Python dependencies or adding any required
config settings.
To install ckanext-keycloak:

1. Activate your CKAN virtual environment, for example::

. /usr/lib/ckan/default/bin/activate

2. Install the ckanext-keycloak Python package into your virtual environment::

pip install ckanext-keycloak

3. Add ``keycloak`` setting in your CKAN config file (by default the config file is located at ``/etc/ckan/default/production.ini``) as follows::

ckan.plugins = keycloak <other-plugins>
ckan.keycloak.authorization_endpoint =
ckan.keycloak.realm =
ckan.keycloak.client_id =
ckan.keycloak.client_secret =
ckan.keycloak.sysadmin_group_name =
ckan.keycloak.profile_group_field =
ckan.keycloak.profile_username_field =
ckan.keycloak.profile_email_field =
ckan.keycloak.profile_fullname_field =

4. Restart CKAN. For example if you've deployed CKAN with Apache on Ubuntu::

sudo service apache2 reload


------------------------
Development Installation
------------------------

To install ckanext-keycloak for development, activate your CKAN virtualenv and
do::

git clone https://github.com/etri-odp/ckanext-keycloak.git
cd ckanext-keycloak
python setup.py develop
pip install -r dev-requirements.txt


-----------------
Running the Tests
-----------------

To run the tests, do::

nosetests --nologcapture --with-pylons=test.ini

To run the tests and produce a coverage report, first make sure you have
coverage installed in your virtualenv (``pip install coverage``) then run::

nosetests --nologcapture --with-pylons=test.ini --with-coverage --cover-package=ckanext.keycloak --cover-inclusive --cover-erase --cover-tests


----------------------------------------------
Registering ckanext-keycloak on PyPI
----------------------------------------------

ckanext-keycloak should be availabe on PyPI as
https://pypi.python.org/pypi/ckanext-keycloak. If that link doesn't work, then
you can register the project on PyPI for the first time by following these
steps:

1. Create a source distribution of the project::

python setup.py sdist

2. Register the project::

python setup.py register

3. Upload the source distribution to PyPI::

python setup.py sdist upload

4. Tag the first release of the project on GitHub with the version number from
the ``setup.py`` file. For example if the version number in ``setup.py`` is
0.0.1 then do::

git tag 0.0.1
git push --tags

================
Acknowledgements
================

This work was supported by Institute for Information & communications Technology Promotion (IITP) grant funded by the Korea government (MSIT) (No.2017-00253, Development of an Advanced Open Data Distribution Platform based on International Standards)
43 changes: 43 additions & 0 deletions bin/travis-build.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#!/bin/bash
set -e

echo "This is travis-build.bash..."

echo "Installing the packages that CKAN requires..."
sudo apt-get update -qq
sudo apt-get install postgresql-$PGVERSION solr-jetty libcommons-fileupload-java:amd64=1.2.2-1

echo "Installing CKAN and its Python dependencies..."
git clone https://github.com/ckan/ckan
cd ckan
export latest_ckan_release_branch=`git branch --all | grep remotes/origin/release-v | sort -r | sed 's/remotes\/origin\///g' | head -n 1`
echo "CKAN branch: $latest_ckan_release_branch"
git checkout $latest_ckan_release_branch
python setup.py develop
pip install -r requirements.txt --allow-all-external
pip install -r dev-requirements.txt --allow-all-external
cd -

echo "Creating the PostgreSQL user and database..."
sudo -u postgres psql -c "CREATE USER ckan_default WITH PASSWORD 'pass';"
sudo -u postgres psql -c 'CREATE DATABASE ckan_test WITH OWNER ckan_default;'

echo "SOLR config..."
# Solr is multicore for tests on ckan master, but it's easier to run tests on
# Travis single-core. See https://github.com/ckan/ckan/issues/2972
sed -i -e 's/solr_url.*/solr_url = http:\/\/127.0.0.1:8983\/solr/' ckan/test-core.ini

echo "Initialising the database..."
cd ckan
paster db init -c test-core.ini
cd -

echo "Installing ckanext-keycloak and its requirements..."
python setup.py develop
pip install -r dev-requirements.txt

echo "Moving test.ini into a subdir..."
mkdir subdir
mv test.ini subdir

echo "travis-build.bash is done."
6 changes: 6 additions & 0 deletions bin/travis-run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/sh -e

echo "NO_START=0\nJETTY_HOST=127.0.0.1\nJETTY_PORT=8983\nJAVA_HOME=$JAVA_HOME" | sudo tee /etc/default/jetty
sudo cp ckan/ckan/config/solr/schema.xml /etc/solr/conf/schema.xml
sudo service jetty restart
nosetests --nologcapture --with-pylons=subdir/test.ini --with-coverage --cover-package=ckanext.keycloak --cover-inclusive --cover-erase --cover-tests
9 changes: 9 additions & 0 deletions ckanext/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# encoding: utf-8

# this is a namespace package
try:
import pkg_resources
pkg_resources.declare_namespace(__name__)
except ImportError:
import pkgutil
__path__ = pkgutil.extend_path(__path__, __name__)
Empty file added ckanext/keycloak/__init__.py
Empty file.
60 changes: 60 additions & 0 deletions ckanext/keycloak/helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# -*- coding: utf-8 -*-

import ckan.model as model
import json
import logging
import os
from six.moves.urllib.parse import urljoin
from base64 import b64encode, b64decode
from pylons import config
from keycloak.realm import KeycloakRealm

log = logging.getLogger(__name__)

class KeycloakHelper(object):

def __init__(self):
self.authorization_endpoint = config.get('ckan.keycloak.authorization_endpoint', None)
self.client_id = config.get('ckan.keycloak.client_id', None)
self.client_secret = config.get('ckan.keycloak.client_secret', None)
self.realm = config.get('ckan.keycloak.realm', 'ckan')
self.profile_username_field = config.get('ckan.keycloak.profile_username_field', None)
self.profile_fullname_field = config.get('ckan.keycloak.profile_fullname_field', None)
self.profile_email_field = config.get('ckan.keycloak.profile_email_field', None)
self.profile_group_field = config.get('ckan.keycloak.profile_group_field', None)
self.sysadmin_group_name = config.get('ckan.keycloak.sysadmin_group_name', None)
realm = KeycloakRealm(server_url=self.authorization_endpoint, realm_name=self.realm)
self.oidc_client = realm.open_id_connect(client_id=self.client_id,client_secret=self.client_secret)

def identify(self, token):
user_token = self.oidc_client.userinfo(token)
user_data = self.oidc_client.decode_token(user_token, '', options={ 'verify_signature': False })
try : email = user_data[self.profile_email_field]
except :
log.debug("Not Found Email.")
try : user_name = user_data[self.profile_username_field]
except :
log.debug("Not Found User Name.")

user = model.User.get(user_name)
if user :
return user.name
user = None
users = model.User.by_email(email)
if len(users) == 1:
user = users[0]
if user is None:
user = model.User(email=email)
user.name = user_name
if self.profile_fullname_field and self.profile_fullname_field in user_data:
user.fullname = user_data[self.profile_fullname_field]
if self.profile_group_field and self.profile_group_field in user_data:
if self.sysadmin_group_name and self.sysadmin_group_name in user_data[self.profile_group_field]:
user.sysadmin = True
else:
user.sysadmin = False
model.Session.add(user)
model.Session.commit()
model.Session.remove()
log.info('Add keycloak user into ckan database: %s'%user)
return user.name
91 changes: 91 additions & 0 deletions ckanext/keycloak/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# -*- coding: utf-8 -*-

from __future__ import unicode_literals

import logging
import helper

from ckan import plugins
from ckan.plugins import toolkit
from pylons import config
from urlparse import urlparse

import ckan.model as model
from ckan.lib.helpers import redirect_to as redirect

log = logging.getLogger(__name__)

class KeycloakPlugin(plugins.SingletonPlugin):
plugins.implements(plugins.IAuthenticator, inherit=True)
plugins.implements(plugins.IConfigurer)

def __init__(self, name=None):
self.keycloak_helper = helper.KeycloakHelper()

def update_config(self,config):
return None

def configure(self, config):
required_keys = (
'ckan.keycloak.authorization_endpoint',
'ckan.keycloak.client_id',
'ckan.keycloak.client_secret',
'ckan.keycloak.realm',
'ckan.keycloak.profile_username_field',
'ckan.keycloak.profile_fullname_field',
'ckan.keycloak.profile_email_field',
'ckan.keycloak.profile_group_field',
'ckan.keycloak.sysadmin_group_name'
)
for key in required_keys:
if config.get(key) is None:
raise RuntimeError('Required configuration option {0} not found.'.format(key))

def identify(self):
if not getattr(toolkit.c, u'user', None):
self._identify_user_default()
if toolkit.c.user and not getattr(toolkit.c, u'userobj', None):
toolkit.c.userobj = model.User.by_name(toolkit.c.user)

def _identify_user_default(self):
toolkit.c.user = toolkit.request.environ.get(u'REMOTE_USER', u'')
if toolkit.c.user:
toolkit.c.user = toolkit.c.user.decode(u'utf8')
toolkit.c.userobj = model.User.by_name(toolkit.c.user)
if toolkit.c.userobj is None or not toolkit.c.userobj.is_active():
ev = request.environ
if u'repoze.who.plugins' in ev:
pth = getattr(ev[u'repoze.who.plugins'][u'friendlyform'],
u'logout_handler_path')
redirect(pth)
else:
toolkit.c.userobj = self._get_user_info()
if 'name' in dir(toolkit.c.userobj) :
toolkit.c.user = toolkit.c.userobj.name
toolkit.c.author = toolkit.c.userobj.name
log.debug('toolkit.c.userobj.id :' + toolkit.c.userobj.id)
log.debug('toolkit.c.userobj.name :' + toolkit.c.userobj.name)

def _get_user_info(self):
authorizationKey = toolkit.request.headers.get(u'Authorization', u'')
if not authorizationKey:
authorizationKey = toolkit.request.environ.get(u'Authorization', u'')
if not authorizationKey:
authorizationKey = toolkit.request.environ.get(u'HTTP_AUTHORIZATION', u'')
if not authorizationKey:
authorizationKey = toolkit.request.environ.get(u'Authorization', u'')
if u' ' in authorizationKey:
authorizationKey = u''
if not authorizationKey:
return None
authorizationKey = authorizationKey.decode(u'utf8', u'ignore')
user = None
query = model.Session.query(model.User)
user = query.filter_by(apikey=authorizationKey).first()
if user == None :
try:
user = self.keycloak_helper.identify(authorizationKey)
user = query.filter_by(name=user).first()
except Exception as e:
log.error( e.message)
return user
1 change: 1 addition & 0 deletions dev-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
-r requirements.txt
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python-keycloak-client==0.1.3
Loading

0 comments on commit a45843c

Please sign in to comment.