Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

flask and python3 conversion #42

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions ckanext/oauth2/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# -*- coding: utf-8 -*-

import click

@click.group()
def oauth2():
"""Oauth2 management commands.
"""
pass
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it needed?



def get_commands():
return [oauth2]
2 changes: 1 addition & 1 deletion ckanext/oauth2/constants.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
CAME_FROM_FIELD = 'came_from'
INITIAL_PAGE = '/dashboard'
INITIAL_PAGE = '/'
REDIRECT_URL = 'oauth2/callback'
6 changes: 2 additions & 4 deletions ckanext/oauth2/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,14 @@
# 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
from ckanext.oauth2 import constants

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 ckanext.oauth2 import oauth2

from ckanext.oauth2.plugin import _get_previous_page

Expand Down
6 changes: 4 additions & 2 deletions ckanext/oauth2/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
# along with OAuth2 CKAN Extension. If not, see <http://www.gnu.org/licenses/>.

import sqlalchemy as sa
from sqlalchemy.ext.declarative import declarative_base

UserToken = None

Base = declarative_base()
metadata = Base.metadata

UserToken = None
def init_db(model):

global UserToken
Expand Down
63 changes: 36 additions & 27 deletions ckanext/oauth2/oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,10 @@
# along with OAuth2 CKAN Extension. If not, see <http://www.gnu.org/licenses/>.


from __future__ import unicode_literals

import base64
import ckan.model as model
import db
from ckanext.oauth2.db import UserToken
import ckanext.oauth2.db as db
import json
import logging
from six.moves.urllib.parse import urljoin
Expand All @@ -38,23 +37,24 @@

import jwt

import constants
from .constants import *
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wildcard import are a bit dangerous and potentially confusing, but they are not a big issue.

from flask import Flask, request, redirect, session, url_for, jsonify



log = logging.getLogger(__name__)


def generate_state(url):
return b64encode(bytes(json.dumps({constants.CAME_FROM_FIELD: url})))
return b64encode(bytes(json.dumps({CAME_FROM_FIELD: url}).encode()))


def get_came_from(state):
return json.loads(b64decode(state)).get(constants.CAME_FROM_FIELD, '/')
return json.loads(b64decode(state)).get(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):
Expand All @@ -79,10 +79,7 @@ def __init__(self):
self.profile_api_groupmembership_field = six.text_type(os.environ.get('CKAN_OAUTH2_PROFILE_API_GROUPMEMBERSHIP_FIELD', toolkit.config.get('ckan.oauth2.profile_api_groupmembership_field', ''))).strip()
self.sysadmin_group_name = six.text_type(os.environ.get('CKAN_OAUTH2_SYSADMIN_GROUP_NAME', toolkit.config.get('ckan.oauth2.sysadmin_group_name', ''))).strip()

self.redirect_uri = urljoin(urljoin(toolkit.config.get('ckan.site_url', 'http://localhost:5000'), toolkit.config.get('ckan.root_path')), constants.REDIRECT_URL)

# Init db
db.init_db(model)
self.redirect_uri = urljoin(urljoin(toolkit.config.get('ckan.site_url', 'http://localhost:5000'), toolkit.config.get('ckan.root_path')), REDIRECT_URL)

missing = [key for key in REQUIRED_CONF if getattr(self, key, "") == ""]
if missing:
Expand All @@ -97,7 +94,7 @@ def challenge(self, came_from_url):
auth_url, _ = oauth.authorization_url(self.authorization_endpoint)
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'))
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 All @@ -111,41 +108,40 @@ def get_token(self):
if self.legacy_idm:
# This is only required for Keyrock v6 and v5
headers['Authorization'] = 'Basic %s' % base64.urlsafe_b64encode(
'%s:%s' % (self.client_id, self.client_secret)
(f'{self.client_id}:{self.client_secret}').encode()
)

try:
log.debug(f'authorization_response: {toolkit.request.url}')
token = oauth.fetch_token(self.token_endpoint,
headers=headers,
client_id=self.client_id,
client_secret=self.client_secret,
authorization_response=toolkit.request.url,
verify=self.verify_https)
authorization_response=toolkit.request.url.replace('http:', 'https:', 1))
except requests.exceptions.SSLError as e:
# TODO search a better way to detect invalid certificates
if "verify failed" in six.text_type(e):
raise InsecureTransportError()
else:
raise

return token

def identify(self, token):

if self.jwt_enable:

log.debug('jwt_enabled')
access_token = bytes(token['access_token'])
user_data = jwt.decode(access_token, verify=False)
user = self.user_json(user_data)
else:

else:
try:
if self.legacy_idm:
profile_response = requests.get(self.profile_api_url + '?access_token=%s' % token['access_token'], verify=self.verify_https)
else:
oauth = OAuth2Session(self.client_id, token=token)
profile_response = oauth.get(self.profile_api_url, verify=self.verify_https)
profile_response = oauth.get(self.profile_api_url)

except requests.exceptions.SSLError as e:
log.debug('exception identify oauth2')
# TODO search a better way to detect invalid certificates
if "verify failed" in six.text_type(e):
raise InsecureTransportError()
Expand All @@ -162,6 +158,7 @@ def identify(self, token):
else:
user_data = profile_response.json()
user = self.user_json(user_data)
log.debug(f'user: {user}')

# Save the user in the database
model.Session.add(user)
Expand All @@ -171,6 +168,7 @@ def identify(self, token):
return user.name

def user_json(self, user_data):
log.debug(f'user_data: {user_data}')
email = user_data[self.profile_api_mail_field]
user_name = user_data[self.profile_api_user_field]

Expand Down Expand Up @@ -214,15 +212,24 @@ def remember(self, user_name):
rememberer = self._get_rememberer(environ)
identity = {'repoze.who.userid': user_name}
headers = rememberer.remember(environ, identity)
response = jsonify()
for header, value in headers:
toolkit.response.headers.add(header, value)
response.headers[header] = value
return response

def redirect_from_callback(self):
def redirect_from_callback(self, resp_remember):
'''Redirect to the callback URL after a successful authentication.'''
state = toolkit.request.params.get('state')
came_from = get_came_from(state)
toolkit.response.status = 302
toolkit.response.location = came_from

response = jsonify()
response.status_code = 302
for header, value in resp_remember.headers:
response.headers[header] = value
response.headers['location'] = came_from
response.autocorrect_location_header = False
return response


def get_stored_token(self, user_name):
user_token = db.UserToken.by_user_name(user_name=user_name)
Expand All @@ -235,8 +242,10 @@ def get_stored_token(self, user_name):
}

def update_token(self, user_name, token):

user_token = db.UserToken.by_user_name(user_name=user_name)
try:
user_token = db.UserToken.by_user_name(user_name=user_name)
except AttributeError as e:
user_token = None
# Create the user if it does not exist
if not user_token:
user_token = db.UserToken()
Expand Down
85 changes: 31 additions & 54 deletions ckanext/oauth2/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +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 oauth2
from .oauth2 import *
import os

from functools import partial
from ckan import plugins
from ckan.common import g
from ckan.plugins import toolkit
from urlparse import urlparse
import ckanext.oauth2.db as db
import urllib.parse
from ckanext.oauth2.views import get_blueprints
from ckanext.oauth2.cli import get_commands

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -62,79 +63,50 @@ 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)
class _OAuth2Plugin(plugins.SingletonPlugin):
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why split the plugin into two?

plugins.implements(plugins.IBlueprint)
plugins.implements(plugins.IClick)

# 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
# IBlueprint

# 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
def get_blueprint(self):
return get_blueprints()

return came_from_url
# IClick

def get_commands(self):
return get_commands()

class OAuth2Plugin(plugins.SingletonPlugin):

class OAuth2Plugin(_OAuth2Plugin, plugins.SingletonPlugin):
plugins.implements(plugins.IAuthenticator, inherit=True)
plugins.implements(plugins.IAuthFunctions, inherit=True)
plugins.implements(plugins.IRoutes, inherit=True)
# plugins.implements(plugins.IRoutes, inherit=True)
plugins.implements(plugins.IConfigurer)


def __init__(self, name=None):
'''Store the OAuth 2 client configuration'''
log.debug('Init OAuth2 extension')

self.oauth2helper = oauth2.OAuth2Helper()

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')
db.init_db(model)
log.debug(f'Creating UserToken...')
self.oauth2helper = OAuth2Helper()

# 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',
controller='ckanext.oauth2.controller:OAuth2Controller',
action='callback')

# Redirect the user to the OAuth service register page
if self.register_url:
m.redirect('/user/register', self.register_url)

# Redirect the user to the OAuth service reset page
if self.reset_url:
m.redirect('/user/reset', self.reset_url)

# Redirect the user to the OAuth service reset page
if self.edit_url:
m.redirect('/user/edit/{user}', self.edit_url)

return m
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Various redirect have been dropped here, but the redirect configuration and test are still here.


def identify(self):
log.debug('identify')

def _refresh_and_save_token(user_name):
new_token = self.oauth2helper.refresh_token(user_name)
if new_token:
toolkit.c.usertoken = new_token
toolkit.g.usertoken = new_token

environ = toolkit.request.environ
apikey = toolkit.request.headers.get(self.authorization_header, '')
user_name = None


if self.authorization_header == "authorization":
if apikey.startswith('Bearer '):
apikey = apikey[7:].strip()
Expand All @@ -146,7 +118,10 @@ def _refresh_and_save_token(user_name):
try:
token = {'access_token': apikey}
user_name = self.oauth2helper.identify(token)
except Exception:
log.debug(f'user_name1: {user_name}')
except Exception as e:
log.debug(f'Auth error:')
log.debug(e)
pass

# If the authentication via API fails, we can still log in the user using session.
Expand All @@ -157,11 +132,12 @@ 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)
toolkit.g.user = user_name
toolkit.g.usertoken = self.oauth2helper.get_stored_token(user_name)
toolkit.g.usertoken_refresh = partial(_refresh_and_save_token, user_name)
else:
g.user = None
toolkit.g.user = None
log.warn('The user is not currently logged...')

def get_auth_functions(self):
Expand All @@ -175,6 +151,7 @@ def get_auth_functions(self):

def update_config(self, config):
# Update our configuration
log.debug('update config...')
self.register_url = os.environ.get("CKAN_OAUTH2_REGISTER_URL", config.get('ckan.oauth2.register_url', None))
self.reset_url = os.environ.get("CKAN_OAUTH2_RESET_URL", config.get('ckan.oauth2.reset_url', None))
self.edit_url = os.environ.get("CKAN_OAUTH2_EDIT_URL", config.get('ckan.oauth2.edit_url', None))
Expand Down
2 changes: 1 addition & 1 deletion ckanext/oauth2/tests/test_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class DBTest(unittest.TestCase):

def setUp(self):
# Restart databse initial status
db.UserToken = None
db.UserToken = None

# Create mocks
self._sa = db.sa
Expand Down
Loading