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

Merge ESGF/COG devel with Python 3 support #1427

Open
wants to merge 82 commits into
base: devel
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
c5a4dd2
Restoring code from outdated oauth integration branch.
watucker Nov 12, 2018
6d4f130
Fixed edit to auth_discover view
watucker Nov 15, 2018
a406807
Added a view for storing the openid_identifier in session before oaut…
watucker Nov 16, 2018
d7af0a7
Adding oauth backend from esgf-auth.
watucker Nov 16, 2018
36f329a
Set OAuth2 client key and secret dynamically
lukaszlacinski Nov 21, 2018
7f8d712
Renamed oauth2_login view.
watucker Nov 21, 2018
54e80de
Remove pysqlite from requirements.txt
mauzey1 May 10, 2019
4770817
Run 2to3 on all Python code
mauzey1 May 10, 2019
0d9354d
Remove periods from imports in the installation Python scripts
mauzey1 May 10, 2019
50a079b
Turn values into strings for ConfigParser
mauzey1 May 10, 2019
5bdde50
Fix indent
mauzey1 May 10, 2019
f5568f8
Remove importing 'join' and 'replace' from module 'string'
mauzey1 May 10, 2019
925d191
Add changes from ESGF devel
mauzey1 May 24, 2019
c26fd56
Replace StringIO.StringIO with io.StringIO
mauzey1 May 24, 2019
7ca5774
Changed ConfigParser.ConfigParser to configparser.ConfigParser
mauzey1 May 24, 2019
e227772
Add "from __future__ import unicode_literals" back to files
mauzey1 May 24, 2019
76498ad
Remove extra line
mauzey1 May 24, 2019
c7473e8
Fix exception and print for Python 3.
mauzey1 May 29, 2019
9ee5e32
Change 'from . import cog.views' to 'import cog.views'
mauzey1 Jun 3, 2019
6348763
Change str and unicode to bytes and text
mauzey1 Jun 4, 2019
6944ad1
Use psycopg2 >=2.7 for Python 3.7 support due to use of 'async' as a …
mauzey1 Jun 7, 2019
7ea2827
Updated packages in requirements.txt; updated Django to 2.2
mauzey1 Jun 7, 2019
6c7253b
change django.core.urlresolvers to django.urls
mauzey1 Jun 7, 2019
15f8e2a
change MIDDLEWARE_CLASSES to MIDDLEWARE in settings.py
mauzey1 Jun 8, 2019
78be6c0
Remove parentheses from User.is_authenticated and User.is_anonymous
mauzey1 Jun 8, 2019
9c002d1
Add on_delete=models.CASCADE to ForeignKey calls
mauzey1 Jul 1, 2019
7d6fdef
Merge branch 'devel' of https://github.com/ESGF/COG into oauth2_slcs
watucker Jul 26, 2019
a426c00
Remove include function for admin URLs
mauzey1 Jul 27, 2019
85ba1f7
Get login and logout from django.contrib.auth
mauzey1 Jul 27, 2019
70983af
Middleware now accepts get_response argument
mauzey1 Jul 27, 2019
81c5863
Replace login and logout with LoginView and LogoutView
mauzey1 Jul 31, 2019
abcc805
Put ignore-case expression (?i) at the end of the regex pattern
mauzey1 Aug 1, 2019
32a9bbc
Replace name of field with queryset=Project.objects.all()
mauzey1 Aug 1, 2019
c1c8896
Replace string decode
mauzey1 Aug 1, 2019
40f78d9
Merge remote-tracking branch 'esgf/devel' into python_3
mauzey1 Aug 1, 2019
7a2cfba
Use floor division when dividing offset by limit when calculating the…
mauzey1 Aug 6, 2019
18a4296
Added view to generate OAuth2 wget scripts for datasets
watucker Aug 12, 2019
f8332ca
Added button for new wget scripts to record displays
watucker Aug 12, 2019
aa45bb1
Re-added old wget script for non OAuth users
watucker Aug 20, 2019
7ed9429
Refactored wget script to allow multi-dataset downloads
watucker Aug 20, 2019
8c18694
Fixed server error from missing session
watucker Aug 22, 2019
d3ca8c4
Changed presentation of new WGET link
watucker Aug 22, 2019
5928a02
Added hover text to WGET links
watucker Aug 22, 2019
001ff76
Merge pull request #6 from EarthSystemCoG/devel
sashakames Sep 9, 2019
ef192a0
Merge branch 'devel' into oauth2_wget_scripts
sashakames Sep 9, 2019
53d9c02
Merge pull request #5 from cedadev/oauth2_wget_scripts
sashakames Sep 9, 2019
80d908b
Update Globus downloads to use Globus SDK
mauzey1 Nov 13, 2019
cd93efb
merge EarthSystemCoG:devel
mauzey1 Nov 25, 2019
2840e59
Add parentheses to print statements
mauzey1 Nov 25, 2019
0d21145
Update requirements.txt package versions
mauzey1 Nov 25, 2019
25e19c1
Merge changes from devel
mauzey1 Dec 5, 2019
467a28e
Apply changes for urllib in Python 3. Change django.core.urlresolver…
mauzey1 Dec 11, 2019
8950f7d
Fix import of views_status in __init__.py
mauzey1 Dec 11, 2019
95c268a
Update reading of 'x-cera-rc' header value when displaying citations.
mauzey1 Dec 12, 2019
3d12704
Update views_globus
mauzey1 Dec 13, 2019
fdb6567
Fix wget script URL so that it has a '?' even if there are no shards …
mauzey1 Dec 13, 2019
53e8027
Merge branch 'devel' of github.com:EarthSystemCoG/COG into python_3
mauzey1 Dec 13, 2019
a3cba24
Merge branch 'devel' of github.com:EarthSystemCoG/COG into python_3
mauzey1 Dec 13, 2019
b1b796c
Comment out esgfpid in requirements.txt
mauzey1 Dec 20, 2019
3ff3e5f
When adding pages or links, make label default to the title if the la…
mauzey1 Jan 15, 2020
f42922d
If the constraints field is blank when configuring search, then store…
mauzey1 Jan 29, 2020
a7b03e0
Revert "Use default search path for searching and displaying files"
mauzey1 Mar 12, 2020
740f8b7
Revert "Use project specific search path for metadata displays"
mauzey1 Mar 12, 2020
b3f5a0e
Revert "Use the default search url for globus file searches"
mauzey1 Mar 12, 2020
9a02f72
Revert "User the default search location as the index node for wget s…
mauzey1 Mar 12, 2020
b946a56
Revert "Ensure the project URL parameter is available to determine co…
mauzey1 Mar 12, 2020
367786e
Refactored ESGFOAuth2 backend to ensure thread-safety
watucker Mar 13, 2020
ada3121
Merge pull request #1 from mauzey1/python_3_revert_index_node_usage
mauzey1 Mar 13, 2020
e87f6c2
Merge pull request #8 from cedadev/devel
sashakames Mar 16, 2020
fa02a80
Merge branch 'python_3' into python_3_devel
watucker Mar 19, 2020
69bdbea
Python 3 fixes for OAuth2 backend
watucker Mar 19, 2020
514d582
Additional fixes for Python 3
watucker Mar 19, 2020
b024cb7
Revert "Fix wget script URL so that it has a '?' even if there are no…
mauzey1 Mar 19, 2020
0b0d305
Added requirements for OAuth2 support
watucker Mar 20, 2020
afb9b41
Merge branch 'python_3' of https://github.com/mauzey1/COG into devel
watucker Mar 20, 2020
863c311
Merge commit '80d908bde0a6b812a23a8a66cdb580004d7bf48a' into cedadev_…
mauzey1 Mar 20, 2020
1fb1162
Merge pull request #1 from mauzey1/cedadev_devel_globus
watucker Mar 23, 2020
053d6be
Merge branch 'python_3' of https://github.com/mauzey1/COG into devel
watucker Mar 24, 2020
bad06b7
Merge branch 'devel' of https://github.com/cedadev/COG into devel
watucker Mar 24, 2020
59e98e7
Adding missing migrations
watucker Mar 25, 2020
35babdd
Removed unneeded social auth setting
watucker Mar 25, 2020
f15feba
Added setting to force social auth redirects to use HTTPS
watucker Jul 8, 2020
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
16 changes: 8 additions & 8 deletions apache/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,18 @@
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")

# print debugging information
print 'Using Python version: %s' % sys.version
print 'Using Python path: %s' % sys.path
print 'PYTHONPATH=%s' % os.environ.get('PYTHONPATH', None)
print 'LD_LIBRARY_PATH=%s' % os.environ.get('LD_LIBRARY_PATH', None)
print 'SSL_CERT_DIR=%s' % os.environ.get('SSL_CERT_DIR', None)
print 'SSL_CERT_FILE=%s' % os.environ.get('SSL_CERT_FILE', None)
print('Using Python version: %s' % sys.version)
print('Using Python path: %s' % sys.path)
print('PYTHONPATH=%s' % os.environ.get('PYTHONPATH', None))
print('LD_LIBRARY_PATH=%s' % os.environ.get('LD_LIBRARY_PATH', None))
print('SSL_CERT_DIR=%s' % os.environ.get('SSL_CERT_DIR', None))
print('SSL_CERT_FILE=%s' % os.environ.get('SSL_CERT_FILE', None))

try:
application = get_wsgi_application()
print 'WSGI without exception'
print('WSGI without exception')
except Exception:
print 'handling WSGI exception'
print('handling WSGI exception')
# Error loading applications
if 'mod_wsgi' in sys.modules:
traceback.print_exc()
Expand Down
Empty file added cog/backends/__init__.py
Empty file.
204 changes: 204 additions & 0 deletions cog/backends/esgf.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
from social_core.backends.oauth import BaseOAuth2
from social_core.backends.open_id import OpenIdAuth, OPENID_ID_FIELD
from social_core.exceptions import AuthMissingParameter
from six.moves.urllib_parse import urlencode, unquote

import os
import logging
from base64 import b64encode
from OpenSSL import crypto
from urllib.parse import urlparse

from openid.yadis.services import getServiceEndpoints
from openid.yadis.discover import DiscoveryFailure

class ESGFOAuth2(BaseOAuth2):
name = 'esgf'
REDIRECT_STATE = True
ACCESS_TOKEN_METHOD = 'POST'
EXTRA_DATA = [
('access_token', 'access_token', True),
('expires_in', 'expires_in', True),
('refresh_token', 'refresh_token', True),
('openid', 'openid', True)
]

@property
def AUTHORIZATION_URL(self):
return self._authorization_url

@property
def ACCESS_TOKEN_URL(self):
return self._access_token_url

@property
def CERTIFICATE_URL(self):
return self._certificate_url

@property
def DEFAULT_SCOPE(self):
return self._default_scope


def __init__(self, strategy=None, redirect_uri=None):

self._authorization_url = None
self._access_token_url = None
self._certificate_url = None
self._default_scope = None

super(ESGFOAuth2, self).__init__(strategy, redirect_uri)

# Get openid_identifier added to the session by PSA. Requires
# SOCIAL_AUTH_FIELDS_STORED_IN_SESSION = ['openid_identifier',]
# set in settings.py
if strategy:
openid = self.strategy.session_get(OPENID_ID_FIELD, None)
self.set_urls(openid)


# get XRDS and extract authorize, access token, and certificate service URLs
def set_urls(self, openid):
try:
uri, endpoints = getServiceEndpoints(openid)
except DiscoveryFailure:
return
for e in endpoints:
if e.matchTypes(['urn:esg:security:oauth:endpoint:authorize']):
self._authorization_url = e.uri
elif e.matchTypes(['urn:esg:security:oauth:endpoint:access']):
self._access_token_url = e.uri
elif e.matchTypes(['urn:esg:security:oauth:endpoint:resource']):
self._certificate_url = e.uri
self._default_scope = [e.uri]



def get_certificate(self, access_token):
""" Generate a new key pair """
key_pair = crypto.PKey()
key_pair.generate_key(crypto.TYPE_RSA, 2048)
self.private_key = crypto.dump_privatekey(crypto.FILETYPE_PEM, key_pair).decode('utf-8')

""" Generate a certificate request """
cert_request = crypto.X509Req()
cert_request.set_pubkey(key_pair)
cert_request.sign(key_pair, 'md5')
cert_request = crypto.dump_certificate_request(crypto.FILETYPE_ASN1, cert_request)

headers = {'Authorization': 'Bearer {}'.format(access_token)}
url = self.CERTIFICATE_URL

request_args = {'headers': headers,
'data': {'certificate_request': b64encode(cert_request)}
}
try:
response = self.request(url, method='POST', **request_args)
response.raise_for_status()
except Exception as e:
logging.error(e)
self.cert = response.text
cert = crypto.load_certificate(crypto.FILETYPE_PEM, response.text)
self.subject = cert.get_subject()
return (self.private_key, self.cert, self.subject.commonName)


# TO DO when OIDC is deployed on ESGF IdP nodes
# extract user info from id_token (OpenID Connect)
# def user_data(self, access_token, *args, **kwargs):
# print 'user_data'
# response = kwargs.get('response')
# id_token = response.get('id_token')
# try:
# decoded_id_token = jwt_decode(id_token, verify=False)
# except (DecodeError, ExpiredSignature) as de:
# raise AuthTokenError(self, de)
# for key in decoded_id_token:
# print '%s:%s' % (key, decoded_id_token[key])
#
# return {'uid': decoded_id_token.get('sub'),
# 'username': decoded_id_token.get('preferred_username'),
# 'name': decoded_id_token.get('name'),
# 'email': decoded_id_token.get('email')
# }


# return values that will be stored in the database as:
# social_auth_usersocialauth.extra_data['openid'],
# auth_user.username
def get_user_details(self, response):
access_token = response.get('access_token')
pkey, cert, openid = self.get_certificate(access_token)
username = os.path.basename(urlparse(openid).path)
return {'openid': openid,
'username': username,
}


# PSA compares returned value with social_auth_usersocialauth.uid (provider=='esgf')
# if it's new, PSA creates a new user
def get_user_id(self, details, response):
return details['openid']


class ESGFOpenId(OpenIdAuth):
name = 'esgf-openid'

# Extend original openid.openid_url() to a case where openid_identifier is set in the session only
def openid_url(self):
"""Return service provider URL.
This base class is generic accepting a POST parameter that specifies
provider URL."""
if self.URL:
return self.URL
elif OPENID_ID_FIELD in self.data:
return self.data[OPENID_ID_FIELD]
elif self.strategy.session_get(OPENID_ID_FIELD, None):
return self.strategy.session_get(OPENID_ID_FIELD, None)
else:
raise AuthMissingParameter(self, OPENID_ID_FIELD)


def get_user_details(self, response):
username = os.path.basename(urlparse(response.identity_url).path)
return {'openid': response.identity_url,
'username': username,
}


def associate_by_openid(backend, details, user=None, *args, **kwargs):
"""
Associate current auth with a user with the same social.uid in the DB.
"""

if user:
return None

openid = details.get('openid')
if openid:
# Try to associate accounts registered with the same OpenId.
# Probably it is possible to get backend.strategy.storage from kwargs['storage'].
if backend.name == 'esgf':
social = backend.strategy.storage.user.get_social_auth(provider='esgf-openid', uid=kwargs['uid'])
elif backend.name == 'esgf-openid':
social = backend.strategy.storage.user.get_social_auth(provider='esgf', uid=kwargs['uid'])
if social:
return {'user': social.user, 'is_new': False}
return None


def discover(openid):
try:
uri, endpoints = getServiceEndpoints(openid)
except DiscoveryFailure:
return None
authorize = None
access = None
for e in endpoints:
if e.matchTypes(['urn:esg:security:oauth:endpoint:authorize']):
authorize = True
elif e.matchTypes(['urn:esg:security:oauth:endpoint:access']):
access = True
if authorize and access:
return 'OAuth2'
return 'OpenID'
2 changes: 1 addition & 1 deletion cog/config/search/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from config_project_search import SearchConfigParser
from .config_project_search import SearchConfigParser
14 changes: 7 additions & 7 deletions cog/config/search/config_project_search.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import sys, os, ConfigParser
import sys, os, configparser
from django.conf import settings
from cog.models import Project, SearchGroup, SearchFacet
from cog.utils import str2bool
Expand All @@ -15,7 +15,7 @@ def __init__(self, project):
def _getConfigParser(self):

# read project configuration
configParser = ConfigParser.RawConfigParser()
configParser = configparser.RawConfigParser()
# must set following line explicitly to preserve the case of configuration keys
configParser.optionxform = str

Expand All @@ -25,7 +25,7 @@ def _getConfigParser(self):
def write(self):
'''Writes the project search configuration to the file $COG_CONFIG_DIR/projects/<project_short_name>/search.cfg'''

print 'Writing search configuration for project=%s' % self.project.short_name
print('Writing search configuration for project=%s' % self.project.short_name)

# load project search profile
project = Project.objects.get(short_name=self.project.short_name)
Expand Down Expand Up @@ -66,23 +66,23 @@ def write(self):
def read(self):
'''Reads the project search configuration from the file $COG_CONFIG_DIR/projects/<project_short_name>/search.cfg'''

print 'Reading search configuration for project=%s' % self.project.short_name
print('Reading search configuration for project=%s' % self.project.short_name)

# load project search profile
project = Project.objects.get(short_name=self.project.short_name)
search_profile = project.searchprofile

# remove existing groups of facets
for group in search_profile.groups.all():
print 'Deleting search group=%s' % group
print('Deleting search group=%s' % group)
group.delete()

# read project configuration
projConfig = self._getConfigParser()
try:
projConfig.read( self.config_file_path )
except Exception as e:
print "Configuration file %s not found" % self.config_file_path
print("Configuration file %s not found" % self.config_file_path)
raise e

# loop over configuration sections
Expand Down Expand Up @@ -110,7 +110,7 @@ def read(self):

for option in projConfig.options(section):
value = projConfig.get(section, option)
print section, option, value
print(section, option, value)
parts = value.split("|")
facet_order = int(option)
facet_key = parts[0]
Expand Down
2 changes: 1 addition & 1 deletion cog/db_migrations/django_openid_auth/0001_initial.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class Migration(migrations.Migration):
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('claimed_id', models.TextField(unique=True, max_length=2047)),
('display_id', models.TextField(max_length=2047)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL)),
('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.CASCADE)),
],
),
]
18 changes: 18 additions & 0 deletions cog/db_migrations/django_openid_auth/0003_auto_20200324_1100.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 2.2.11 on 2020-03-24 11:00

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('django_openid_auth', '0002_auto_20160106_0812'),
]

operations = [
migrations.AlterField(
model_name='useropenid',
name='claimed_id',
field=models.TextField(max_length=2047),
),
]
20 changes: 10 additions & 10 deletions cog/forms/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
from forms_image import *
from forms_account import *
from forms_bookmarks import *
from forms_governance import *
from forms_others import *
from forms_post import *
from forms_project import *
from forms_aboutus import *
from forms_search import *
from forms_access_control import *
from .forms_image import *
from .forms_account import *
from .forms_bookmarks import *
from .forms_governance import *
from .forms_others import *
from .forms_post import *
from .forms_project import *
from .forms_aboutus import *
from .forms_search import *
from .forms_access_control import *
10 changes: 8 additions & 2 deletions cog/forms/forms_account.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,12 @@ class Meta:
fields = ('claimed_id',)


class OAuth2LoginForm(Form):

openid_identifier = CharField()
next = CharField(required=False)


class PasswordResetForm(Form):

openid = CharField(required=True, widget=TextInput(attrs={'size': '50'}))
Expand Down Expand Up @@ -286,7 +292,7 @@ def validate_username(form, user_id):

# once the openid is validated, choose the closest possible username
_username = createUsername(username)
print 'Created username=%s from=%s' % (_username, username)
print('Created username=%s from=%s' % (_username, username))
cleaned_data['username'] = _username # override form data
else:
# django will automatically check that the username is unique in the CoG database
Expand All @@ -304,6 +310,6 @@ def validate_field(form, field_name, field_value):
def validate_ascii(form, field_name, field_value):
if field_value:
try:
field_value.decode('ascii')
field_value.encode('ascii')
except (UnicodeDecodeError, UnicodeEncodeError):
form._errors[field_name] = form.error_class(["'%s' contains invalid characters." % field_name])
2 changes: 1 addition & 1 deletion cog/forms/forms_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,6 @@ def clean(self):
self._errors["image"] = self.error_class(["Image size exceeds the maximum allowed."])
except OSError as e:
# image not existing on disk
print e
print(e)

return self.cleaned_data
Loading