Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ ckan.oauth2.authorization_header = OAUTH2_HEADER
> ckan.oauth2.authorization_header = Authorization
> ```
>
> And this is an example for using Google as OAuth2 provider:
> And this is an theme for using Google as OAuth2 provider:
Copy link
Contributor

Choose a reason for hiding this comment

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

There are a lot of places where the word example is replaced by theme. Why?

>
> ```
> ## OAuth2 configuration
Expand Down
10 changes: 5 additions & 5 deletions LICENSE.txt
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
which are not part of the work. For theme, Corresponding Source
Copy link

Choose a reason for hiding this comment

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

LICENSE.txt should not be modified.

includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
Expand Down Expand Up @@ -311,7 +311,7 @@ fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
modified object code on the User Product (for theme, the work has
been installed in ROM).

The requirement to provide Installation Information does not include a
Expand Down Expand Up @@ -449,7 +449,7 @@ Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.

You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
rights granted or affirmed under this License. For theme, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
Expand Down Expand Up @@ -532,7 +532,7 @@ otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
not convey it at all. For theme, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
Expand Down Expand Up @@ -649,7 +649,7 @@ Also add information on how to contact you by electronic and paper mail.

If your software can interact with users remotely through a computer
network, you should also make sure that it provides a way for users to
get its source. For example, if your program is a web application, its
get its source. For theme, if your program is a web application, its
interface could display a "Source" link that leads users to an archive
of the code. There are many ways you could offer source, and different
solutions will be better for different programs; see section 13 for the
Expand Down
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: 3 additions & 3 deletions ckanext/oauth2/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,16 +18,16 @@
# 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
# from __future__ import unicode_literals
Copy link

Choose a reason for hiding this comment

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

Shouldn't be dropped entirely?


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
48 changes: 29 additions & 19 deletions ckanext/oauth2/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,42 @@
# along with OAuth2 CKAN Extension. If not, see <http://www.gnu.org/licenses/>.

import sqlalchemy as sa
import ckan.model.meta as meta
import logging
Copy link

Choose a reason for hiding this comment

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

There is no logging in this file, except a getLogger. I suggest we discard this.

from ckan.model.domain_object import DomainObject
from sqlalchemy.ext.declarative import declarative_base

UserToken = None
log = logging.getLogger(__name__)
Copy link

Choose a reason for hiding this comment

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

Not used, can be discarded.


Base = declarative_base()
metadata = Base.metadata

def init_db(model):

global UserToken
if UserToken is None:
class UserToken(Base, DomainObject):
__tablename__ = 'user_token'

class _UserToken(model.DomainObject):
def __init__(self, user_name, access_token, token_type, refresh_token, expires_in):
self.user_name = user_name
self.access_token = access_token
self.token_type = token_type
self.refresh_token = refresh_token
self.expires_in = expires_in

@classmethod
def by_user_name(cls, user_name):
return model.Session.query(cls).filter_by(user_name=user_name).first()
@classmethod
def by_user_name(cls, user_name):
return meta.Session.query(cls).filter_by(user_name=user_name).first()

UserToken = _UserToken

user_token_table = sa.Table('user_token', model.meta.metadata,
sa.Column('user_name', sa.types.UnicodeText, primary_key=True),
sa.Column('access_token', sa.types.UnicodeText),
sa.Column('token_type', sa.types.UnicodeText),
sa.Column('refresh_token', sa.types.UnicodeText),
sa.Column('expires_in', sa.types.UnicodeText)
)
user_name = sa.Column(sa.types.UnicodeText, primary_key=True)
access_token = sa.Column(sa.types.UnicodeText)
token_type = sa.Column(sa.types.UnicodeText)
refresh_token = sa.Column(sa.types.UnicodeText)
expires_in = sa.Column(sa.types.UnicodeText)

# Create the table only if it does not exist
user_token_table.create(checkfirst=True)

model.meta.mapper(UserToken, user_token_table)


Copy link

Choose a reason for hiding this comment

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

These empty lines could be avoided.

# # Create the table only if it does not exist
# user_token_table.create(checkfirst=True)

# model.meta.mapper(UserToken, user_token_table)
93 changes: 59 additions & 34 deletions ckanext/oauth2/oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
# along with OAuth2 CKAN Extension. If not, see <http://www.gnu.org/licenses/>.


from __future__ import unicode_literals
# from __future__ import unicode_literals
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of commenting, just drop the line. This happens a lot of times


import base64
import ckan.model as model
import db
from ckanext.oauth2.db import UserToken
import json
import logging
from six.moves.urllib.parse import urljoin
Expand All @@ -38,23 +38,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 +80,8 @@ 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)
self.redirect_uri = urljoin(urljoin(toolkit.config.get('ckan.site_url', 'http://localhost:5000'), toolkit.config.get('ckan.root_path')), REDIRECT_URL)

# Init db
db.init_db(model)

missing = [key for key in REQUIRED_CONF if getattr(self, key, "") == ""]
if missing:
Expand All @@ -93,11 +92,12 @@ def __init__(self):
def challenge(self, came_from_url):
# This function is called by the log in function when the user is not logged in
state = generate_state(came_from_url)
# log.debug(f'redirect uri: {self.redirect_uri}')
Copy link

Choose a reason for hiding this comment

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

It should be removed.

oauth = OAuth2Session(self.client_id, redirect_uri=self.redirect_uri, scope=self.scope, state=state)
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,39 +111,45 @@ 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'self.token_endpoint: {self.token_endpoint}')
# log.debug(f'headers: {headers}')
# log.debug(f'authorization_response: {toolkit.request.url}')
# log.debug(f'client_secret: {self.client_secret}')
Copy link
Contributor

Choose a reason for hiding this comment

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

Are these lines required?

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)
# verify=self.verify_https
# headers=headers,
# log.debug(f'token: {token}')
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:

try:
if self.legacy_idm:
profile_response = requests.get(self.profile_api_url + '?access_token=%s' % token['access_token'], verify=self.verify_https)
log.debug(f'profile response: {profile_response}')
Copy link

Choose a reason for hiding this comment

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

Should we keep this?

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)
log.debug(f'profile response_: {profile_response}')
Copy link

Choose a reason for hiding this comment

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

Should we keep this?


except requests.exceptions.SSLError as e:
# TODO search a better way to detect invalid certificates
Expand All @@ -162,6 +168,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 Down Expand Up @@ -214,18 +221,33 @@ 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
# toolkit.response.status = 302
# toolkit.response.location = came_from
# return toolkit.redirect_to(came_from)
# log.debug(f'come from: {came_from}')
# toolkit.response.status = 302
# toolkit.response.location = came_from
Copy link
Contributor

Choose a reason for hiding this comment

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

Remove if not required

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)
# log.debug(f'user_name: {user_name}')
user_token = UserToken.by_user_name(user_name=user_name)
if user_token:
return {
'access_token': user_token.access_token,
Expand All @@ -235,22 +257,25 @@ 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 = UserToken.by_user_name(user_name=user_name)
except AttributeError as e:
user_token = None
# log.debug(f'User_token: {user_token}')
# Create the user if it does not exist
if not user_token:
user_token = db.UserToken()
user_token.user_name = user_name
# Save the new token
user_token.access_token = token['access_token']
user_token.token_type = token['token_type']
user_token.refresh_token = token.get('refresh_token')
access_token = token['access_token']
token_type = token['token_type']
refresh_token = token.get('refresh_token')
if 'expires_in' in token:
user_token.expires_in = token['expires_in']
expires_in = token['expires_in']
else:
access_token = jwt.decode(user_token.access_token, verify=False)
user_token.expires_in = access_token['exp'] - access_token['iat']

expires_in = access_token['exp'] - access_token['iat']
if not user_token:
user_token = UserToken(user_name, access_token, token_type, refresh_token, expires_in)
log.debug('user addedd')
# log.debug(f'User_token: {user_token}')
model.Session.add(user_token)
model.Session.commit()

Expand Down
Loading