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

Upload authentication #92

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
10 changes: 5 additions & 5 deletions tests/adapters/usgs/test_eros_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def test_token_present(self, mockPost, mockToken):
@patch('theia.adapters.usgs.ErosWrapper.eros_post', return_value={'data': 'aaaaaa'})
def test_successful_connect(self, mockPost, mockToken, mockPassword, mockUsername):
result = ErosWrapper.connect()
mockPost.assert_called_once_with('login', {'username': 'u', 'password': 'p'})
mockPost.assert_called_once_with('login', {'username': 'u', 'password': 'p'}, authenticating=True)

@patch('theia.adapters.usgs.ErosWrapper.token', return_value=None)
@patch('theia.adapters.usgs.ErosWrapper.eros_post', return_value={'errorCode': 1})
Expand Down Expand Up @@ -84,7 +84,7 @@ def test_eros_prepare(self):
result = ErosWrapper.eros_prepare({'key': 'value'}, params={'foo': 'bar'})
assert result == {
'headers': {'Content-Type': 'application/json', 'X-Auth-Token': 'aaaaaa'},
'params': {'foo': 'bar', 'jsonRequest': '{"key": "value"}'}
'params': {'jsonRequest': '{"key": "value"}'}
}

result = ErosWrapper.eros_prepare({'key': 'value'}, headers={'X-Foo': 'bar'})
Expand All @@ -103,7 +103,7 @@ def test_eros_post(self, mockPost, mockUrl, mockPrepare, mockConnect):
result = ErosWrapper.eros_post('endpoint', {'foo': 'bar'}, thing='thing')

mockConnect.assert_called_once()
mockPrepare.assert_called_once_with({'foo': 'bar'}, thing='thing')
mockPrepare.assert_called_once_with({'foo': 'bar'}, authenticating=False, thing='thing')
mockUrl.assert_called_once_with('endpoint')
mockPost.assert_called_once_with('api_url', prepare='result')
assert(result=='some json string')
Expand All @@ -118,13 +118,13 @@ def test_eros_get(self, mockPost, mockUrl, mockPrepare, mockConnect):
result = ErosWrapper.eros_get('endpoint', {'foo': 'bar'}, thing='thing')

mockConnect.assert_called_once()
mockPrepare.assert_called_once_with({'foo': 'bar'}, thing='thing')
mockPrepare.assert_called_once_with({'foo': 'bar'}, authenticating=False, thing='thing')
mockUrl.assert_called_once_with('endpoint')
mockPost.assert_called_once_with('api_url', prepare='result')
assert(result=='some json string')

def test_api_url(self):
assert(ErosWrapper.api_url('foo')=='https://earthexplorer.usgs.gov/inventory/json/v/stable/foo')
assert(ErosWrapper.api_url('foo')=='https://earthexplorer.usgs.gov/inventory/json/v/1.3.0/foo')

@patch('theia.adapters.usgs.ErosWrapper.eros_post', return_value={'data': 'foo'})
def test_search_once(self, mockPost):
Expand Down
46 changes: 29 additions & 17 deletions theia/adapters/usgs/eros_wrapper.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from .utils import Utils
from urllib.parse import urljoin
from urllib.parse import urljoin, quote
from os import environ
import json
import requests
Expand All @@ -20,7 +20,7 @@ def connect(cls):
response = cls.eros_post('login', {
'username': Utils.get_username(),
'password': Utils.get_password()
})
}, authenticating=True)

if not response.get('errorCode'):
cls.auth_token = response.get('data')
Expand Down Expand Up @@ -52,7 +52,7 @@ def search_once(cls, search):
@classmethod
def api_url(cls, path):
# return urljoin('https://demo1580318.mockable.io/', path)
return urljoin('https://earthexplorer.usgs.gov/inventory/json/v/stable/', path)
return urljoin('https://earthexplorer.usgs.gov/inventory/json/v/1.3.0/', path)

@classmethod
def parse_result_set(cls, result_set):
Expand All @@ -62,40 +62,52 @@ def parse_result_set(cls, result_set):
return [scene.get('displayId', None) for scene in result_set if 'displayId' in scene]

@classmethod
def eros_get(cls, url, request_data, **kwargs):
def eros_get(cls, url, request_data, authenticating=False, **kwargs):
if url != 'login' and (not cls.token()):
cls.connect()

new_args = cls.eros_prepare(request_data, **kwargs)
new_args = cls.eros_prepare(request_data, authenticating=authenticating, **kwargs)
return requests.get(cls.api_url(url), **new_args).json()

@classmethod
def eros_post(cls, url, request_data, **kwargs):
def eros_post(cls, url, request_data, authenticating=False, **kwargs):
if url != 'login' and (not cls.token()):
cls.connect()

new_args = cls.eros_prepare(request_data, **kwargs)
new_args = cls.eros_prepare(request_data, authenticating=authenticating, **kwargs)
return requests.post(cls.api_url(url), **new_args).json()

@classmethod
def eros_prepare(cls, request_data, **kwargs):
headers = {'Content-Type': 'application/json'}
def eros_prepare(cls, request_data, authenticating=False, **kwargs, ):
headers = {}
if cls.token():
headers['X-Auth-Token'] = cls.token()
if 'headers' in kwargs:
headers = {**headers, **(kwargs['headers'])}

params = {}

if request_data:
params = {'jsonRequest': json.dumps(request_data)}

if 'params' in kwargs:
params = {**params, **(kwargs['params'])}

new_args = {
'headers': headers,
'params': params,
}
if authenticating:
headers['Content-Type'] = 'application/x-www-form-urlencoded'

if request_data:
data = 'jsonRequest=' + quote(json.dumps(request_data))

new_args = {
'headers': headers,
'data': data,
}
else:
headers['Content-Type'] = 'application/json'

if request_data:
params = {'jsonRequest': json.dumps(request_data)}

new_args = {
'headers': headers,
'params': params,
}

return {**kwargs, **new_args}
4 changes: 4 additions & 0 deletions theia/api/models/imagery_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ class ImageryRequest(models.Model):
project = models.ForeignKey(Project, related_name='imagery_requests', on_delete=models.CASCADE)
pipeline = models.ForeignKey(Pipeline, related_name='imagery_requests', on_delete=models.CASCADE)

bearer_token = models.CharField(max_length=2048, default="tist")
Copy link
Member

Choose a reason for hiding this comment

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

It's kind of hard to tell--does this actually store the tokens in the db as fields on an imagery_request table? Or are these just built in memory, per-request?

refresh_token = models.CharField(max_length=512, default="TAST")
bearer_expiry = models.CharField(max_length=128, default="TESTO")

def __str__(self):
when = self.created_at or datetime.utcnow()
return '[ImageryRequest project %d at %s]' % (self.project_id, when.strftime('%F'))
Expand Down
9 changes: 8 additions & 1 deletion theia/api/views/home.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
from datetime import datetime, timedelta
from django.contrib.auth.decorators import login_required
from django.shortcuts import render


@login_required
def home(request):
social_user = request.user.social_auth.get(provider="panoptes")

request.session['bearer_token'] = social_user.extra_data['access_token']
request.session['refresh_token'] = social_user.extra_data['refresh_token']

bearer_expiry = str(datetime.now() + timedelta(seconds=social_user.extra_data['expires_in']))
request.session['bearer_expiry'] = bearer_expiry

context = {
"projects": social_user.extra_data['projects']
}
Expand Down
16 changes: 16 additions & 0 deletions theia/api/views/imagery_request_view_set.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
from theia.api.models import ImageryRequest
from rest_framework import viewsets
from rest_framework.response import Response
from rest_framework import status

from theia.api.serializers import ImageryRequestSerializer


class ImageryRequestViewSet(viewsets.ModelViewSet):
queryset = ImageryRequest.objects.all()
serializer_class = ImageryRequestSerializer

def create(self, request, *args, **kwargs):
copy = request.data.copy()
copy['bearer_token'] = request.session['bearer_token']
copy['refresh_token'] = request.session['refresh_token']
copy['bearer_expiry'] = request.session['bearer_expiry']

serializer = self.get_serializer(data=copy)
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)

headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
29 changes: 17 additions & 12 deletions theia/operations/panoptes_operations/upload_subject.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,25 @@ def apply(self, filenames):
else:
scope = self.pipeline

self._connect()

target_set = self._get_subject_set(scope, self.project.id, scope.name_subject_set())
for filename in filenames:
new_subject = self._create_subject(self.project.id, filename)
target_set.add(new_subject)

def _connect(self):
Panoptes.connect(
endpoint=PanoptesUtils.base_url(),
client_id=PanoptesUtils.client_id(),
client_secret=PanoptesUtils.client_secret()
self.authenticated_panoptes = Panoptes(
endpoint=PanoptesUtils.base_url(),
client_id=PanoptesUtils.client_id(),
client_secret=PanoptesUtils.client_secret()
)

self.authenticated_panoptes.bearer_token = self.imagery_request.bearer_token
self.authenticated_panoptes.logged_in = True
self.authenticated_panoptes.refresh_token = self.imagery_request.refresh_token
bearer_expiry = datetime.strptime(self.imagery_request.bearer_expiry, "%Y-%m-%d %H:%M:%S.%f")
self.authenticated_panoptes.bearer_expires = (bearer_expiry)

with self.authenticated_panoptes:
target_set = self._get_subject_set(scope, self.project.id, scope.name_subject_set())

for filename in filenames:
new_subject = self._create_subject(self.project.id, filename)
target_set.add(new_subject)

def _get_subject_set(self, scope, project_id, set_name):
subject_set = None
if not scope.subject_set_id:
Expand Down
4 changes: 2 additions & 2 deletions theia/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,5 +180,5 @@
LOGIN_REDIRECT_URL = 'home'
LOGOUT_REDIRECT_URL = 'home'

SOCIAL_AUTH_PANOPTES_KEY = os.getenv('PANOPTES_CLIENT_ID')
SOCIAL_AUTH_PANOPTES_SECRET = os.getenv('PANOPTES_CLIENT_SECRET')
SOCIAL_AUTH_PANOPTES_KEY = os.getenv('PANOPTES_PROD_CLIENT_ID')
SOCIAL_AUTH_PANOPTES_SECRET = os.getenv('PANOPTES_PROD_CLIENT_SECRET')
22 changes: 15 additions & 7 deletions theia/utils/panoptes_oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,21 @@ class PanoptesOAuth2(BaseOAuth2):
]

def get_user_details(self, response):
with Panoptes() as p:
p.bearer_token = response['access_token']
p.logged_in = True
p.refresh_token = response['refresh_token']
p.bearer_expires = (datetime.now() + timedelta(seconds=response['expires_in']))
authenticated_panoptes = Panoptes(
Copy link
Contributor

Choose a reason for hiding this comment

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

Noting that this request will return the user details of zooniverse oauth application owner, not the actual user that is initiating the request (unless they own the oauth application).

Unless i'm missing how the Panoptes social module works, normally client creds flow returns the token for the owning user.

endpoint=PanoptesUtils.base_url(),
client_id=PanoptesUtils.client_id(),
client_secret=PanoptesUtils.client_secret()
)

user = p.get('/me')[0]['users'][0]
authenticated_panoptes.bearer_token = response['access_token']
authenticated_panoptes.logged_in = True
authenticated_panoptes.refresh_token = response['refresh_token']

bearer_expiry = datetime.now() + timedelta(seconds=response['expires_in'])
authenticated_panoptes.bearer_expires = (bearer_expiry)

with authenticated_panoptes:
user = authenticated_panoptes.get('/me')[0]['users'][0]

ids = ['admin user']
if not user['admin']:
Expand All @@ -38,7 +46,7 @@ def get_user_details(self, response):
'username': user['login'],
'email': user['email'],
'is_superuser': user['admin'],
'projects': ids,
'projects': ids
}

def get_user_id(self, details, response):
Expand Down
6 changes: 3 additions & 3 deletions theia/utils/panoptes_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
class PanoptesUtils:
@classmethod
def client_id(cls):
return os.getenv('PANOPTES_CLIENT_ID')
return os.getenv('PANOPTES_PROD_CLIENT_ID')

@classmethod
def client_secret(cls):
return os.getenv('PANOPTES_CLIENT_SECRET')
return os.getenv('PANOPTES_PROD_CLIENT_SECRET')

@classmethod
def url(cls, path):
return urljoin(cls.base_url(), path)

@classmethod
def base_url(cls):
return os.getenv('PANOPTES_URL', 'https://panoptes.zooniverse.org/')
return os.getenv('PANOPTES_PROD_URL')