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

Reimplement Authentication #94

Merged
merged 8 commits into from
Mar 26, 2020
Merged
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: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export USGS_USERNAME=something_secret_goes_here
export USGS_PASSWORD=something_secret_goes_here

export PANOPTES_PROD_CLIENT_ID=something_secret_goes_here
Copy link
Contributor

Choose a reason for hiding this comment

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

Can these just be collapsed to non deployed environment keys and allow the deploying environment to inject the correct value? E.g. PANOPTES_PROD_CLIENT_ID becomes PANOPTES_CLIENT_ID where the value is specified differently in staging & production

Copy link
Contributor Author

@chelseatroy chelseatroy Mar 26, 2020

Choose a reason for hiding this comment

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

Hm; well, this is sort of an interim step.

When I inherited theia, she could only connect to production version of panoptes. Not staging. I would like to get to the point where she can also upload subjects to staging for testing out changes without affecting data on prod, and I think we're almost there.

In the meantime, I have this set up so that I can switch between pointing at projects on prod Panoptes and staging Panoptes with my local theia, and not have to keep copy and pasting over the keys as I am developing.

For deployment, we can collapse them.

export PANOPTES_PROD_CLIENT_SECRET=something_secret_goes_here
export PANOPTES_PROD_URL=https://panoptes.zooniverse.org/

export PANOPTES_STAGING_CLIENT_ID=something_secret_goes_here
export PANOPTES_STAGING_CLIENT_SECRET=something_secret_goes_here
export PANOPTES_STAGING_URL=https://panoptes-staging.zooniverse.org/
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dist/
downloads/
eggs/
.eggs/
.env
lib/
lib64/
parts/
Expand Down
1 change: 1 addition & 0 deletions conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@

Copy link
Contributor

Choose a reason for hiding this comment

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

is this file required?

Copy link
Contributor Author

@chelseatroy chelseatroy Mar 26, 2020

Choose a reason for hiding this comment

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

It is; without it, pytest is unable to locate the tests unless they are at a very specific file path; it's apparently a known thing, and this is the workaround.

7 changes: 6 additions & 1 deletion tests/operations/image_operations/test_compose_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,9 @@ def test_apply(self, mock_merge, mock_open, mock_get_new, mock_get_version):

mock_open.assert_has_calls([call('green_tif'), call('red_tif'), call('blue_tif')])
mock_merge.assert_called_once_with('RGB', (ANY, ANY, ANY))
mock_merge.return_value.save.assert_called_once_with('/Users/chelseatroy/workspace/theia/3_/totally new name')
mock_merge.return_value.save.assert_called_once()
first_method_call = mock_merge.return_value.save.call_args_list[0]
args = first_method_call[0]
assert str.endswith(args[0], '/theia/3_/totally new name')


7 changes: 6 additions & 1 deletion tests/operations/image_operations/test_remap_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,9 @@ def test_do_apply(self, mockFromArray, mockOpen, mockRename, mockRemap):
mockOpen.return_value.read_image.assert_called_once_with()
mockRemap.assert_called_once_with(self.dummy_array)
mockFromArray.assert_called_once_with(self.dummy_array)
mockFromArray.return_value.save.assert_called_once_with('/Users/chelseatroy/workspace/theia/3_/versioned filename')

mockFromArray.return_value.save.assert_called_once()
first_method_call = mockFromArray.return_value.save.call_args_list[0]
args = first_method_call[0]
assert str.endswith(args[0], '/theia/3_/versioned filename')

5 changes: 4 additions & 1 deletion tests/operations/image_operations/test_resize_image.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,7 @@ def test_apply(self, mock_open):

mock_open.assert_called_once_with('literal filename')
mock_open.return_value.thumbnail.assert_called_once_with((10, 20), Image.ANTIALIAS)
mock_open.return_value.save.assert_called_once_with('/Users/chelseatroy/workspace/theia/3_/literal filename_resized_to_10_20.tif')
mock_open.return_value.save.assert_called_once()
first_method_call = mock_open.return_value.save.call_args_list[0]
args = first_method_call[0]
assert str.endswith(args[0], 'theia/3_/literal filename_resized_to_10_20.tif')
8 changes: 6 additions & 2 deletions tests/operations/panoptes_operations/test_upload_subject.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ class TestUploadSubject:
@patch('theia.operations.panoptes_operations.UploadSubject._get_subject_set', return_value=SubjectSet())
@patch('theia.operations.panoptes_operations.UploadSubject._create_subject', return_value=Subject())
@patch('panoptes_client.SubjectSet.add')
def test_apply_single(self, mockAdd, mockCreate, mockGet, mockGetName, *args):
@patch('PIL.Image.open', return_value=Mock())
def test_apply_single(self, mockOpen, mockAdd, mockCreate, mockGet, mockGetName, *args):
project = Project(id=8)
pipeline = Pipeline(project=project)
bundle = JobBundle(pipeline=pipeline)

operation = UploadSubject(bundle)
operation.apply(['some_file'])

mockOpen.assert_called_once()
mockGetName.assert_called_once()
mockGet.assert_called_once_with(pipeline, 8, 'pipeline name')
mockCreate.assert_called_once_with(8, 'some_file')
Expand All @@ -30,14 +32,16 @@ def test_apply_single(self, mockAdd, mockCreate, mockGet, mockGetName, *args):
@patch('theia.operations.panoptes_operations.UploadSubject._get_subject_set', return_value=SubjectSet())
@patch('theia.operations.panoptes_operations.UploadSubject._create_subject', return_value=Subject())
@patch('panoptes_client.SubjectSet.add')
def test_apply_multiple(self, mockAdd, mockCreate, mockGet, mockGetName, *args):
@patch('PIL.Image.open', return_value=Mock())
def test_apply_multiple(self, mockOpen, mockAdd, mockCreate, mockGet, mockGetName, *args):
project = Project(id=8)
pipeline = Pipeline(project=project, multiple_subject_sets=True)
bundle = JobBundle(pipeline=pipeline)

operation = UploadSubject(bundle)
operation.apply(['some_file'])

mockOpen.assert_called_once()
mockGetName.assert_called_once()
mockGet.assert_called_once_with(bundle, 8, 'bundle name')
mockCreate.assert_called_once_with(8, 'some_file')
Expand Down
5 changes: 4 additions & 1 deletion tests/test_theia_tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,10 @@ def test_process_bundle(mock_input_files, mock_resolve, mock_apply, mock_retriev
tasks.process_bundle(3)

assert (mock_apply.call_count == 2)
mock_apply.assert_called_with(['/Users/chelseatroy/workspace/theia/1_noop/input_file'])
second_method_call = mock_apply.call_args_list[1]
arguments = second_method_call[0]
file_list = arguments[0]
assert str.endswith(file_list[0], '/theia/1_noop/input_file')

mock_retrieve.assert_called_once()

Expand Down
6 changes: 4 additions & 2 deletions tests/utils/test_panoptes_oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@

from theia.utils import PanoptesOAuth2

class FakeProject(NamedTuple):
id: str
class FakeProject():
def __init__(self, id, href='default.org'):
self.id = id
self.href = href

class TestPanoptesOauth2:
def test_get_user_id(self):
Expand Down
4 changes: 0 additions & 4 deletions theia/api/models/imagery_request.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,6 @@ 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")
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
1 change: 0 additions & 1 deletion theia/api/templates/api.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
{% extends 'base.html' %}

{% block site_wrapper %}
<h1>API</h1>
<div class="content">
{% block content %}
{% endblock %}
Expand Down
20 changes: 20 additions & 0 deletions theia/api/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,26 @@
{{ site_name }}{% endblock %}</title>
<meta name="keywords" content="{{ meta_keywords }}" />
<meta name="description" content="{{ meta_description }}" />
<style>
body {
background-image: url('https://images.unsplash.com/photo-1531707566548-6577aab321d7?ixlib=rb-1.2.1');
background-repeat: no-repeat;
background-size: cover;
}
h1, h2, h3 {
color: white;
}
p {
color: teal;
}
.theia-button {
background-color: teal;
padding: 10px;
margin: 10px;
corner-radius: 5px;
color: white;
}
</style>
</head>
<body>
{% block site_wrapper %}
Expand Down
12 changes: 8 additions & 4 deletions theia/api/templates/home.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
{% extends 'api.html' %}

{% block content %}
<h2>Homepage</h2>
<p>Hello {{ user.username }}!</p>
<h3>Projects</h3>
<h2>Hello, {{ user.username }}!</h2>
<p>Welcome to the NASA Landsat data processing pipeline.</p>

<h3>Here are your projects:</h3>
<p>{{ projects }}</p>
<a href="{% url 'logout' %}">log out</a>

<a href="{% url 'imageryrequest-list' %}" class="theia-button">Run a Pipeline!</a>

<a href="{% url 'logout' %}" class="theia-button">Sign Out</a>
{% endblock %}
4 changes: 2 additions & 2 deletions theia/api/templates/registration/login.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{% extends 'api.html' %}

{% block content %}
<h2>Login</h2>
<a href="{% url 'social:begin' 'panoptes' %}">Login with Panoptes</a><br>
<h2>Hi. We're glad you're here.</h2>
<a href="{% url 'social:begin' 'panoptes' %}" class="theia-button">Sign in with Zooniverse</a><br>
{% endblock %}
44 changes: 35 additions & 9 deletions theia/api/views/imagery_request_view_set.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
from rest_framework.response import Response
from rest_framework import status

from datetime import datetime
from panoptes_client import Panoptes, Project
from panoptes_client.panoptes import PanoptesAPIException

from theia.utils.panoptes_utils import PanoptesUtils
from theia.api.serializers import ImageryRequestSerializer


Expand All @@ -11,14 +16,35 @@ class ImageryRequestViewSet(viewsets.ModelViewSet):
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 = self.get_serializer(data=request.data)
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)
with get_authenticated_panoptes(
request.session['bearer_token'],
request.session['bearer_expiry']):
try:
Project.find(_id_for(request.data['project']))
self.perform_create(serializer)

headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
except PanoptesAPIException:
return Response(serializer.data, status=status.HTTP_401_UNAUTHORIZED, headers=headers)


def get_authenticated_panoptes(bearer_token, bearer_expiry):
guest_authenticated_panoptes = Panoptes(
endpoint=PanoptesUtils.base_url()
)

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

return guest_authenticated_panoptes


def _id_for(project_url):
components = list(filter(lambda x: x != '', str.split(project_url, "/")))
return components[len(components) - 1]
17 changes: 6 additions & 11 deletions theia/operations/panoptes_operations/upload_subject.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,21 @@

from datetime import datetime


class UploadSubject(AbstractOperation):
def apply(self, filenames):
if self.pipeline.multiple_subject_sets:
scope = self.bundle
else:
scope = self.pipeline

self.authenticated_panoptes = Panoptes(
endpoint=PanoptesUtils.base_url(),
client_id=PanoptesUtils.client_id(),
client_secret=PanoptesUtils.client_secret()
theia_authenticated_client = 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:
with theia_authenticated_client:
target_set = self._get_subject_set(scope, self.project.id, scope.name_subject_set())

for filename in filenames:
Expand Down
5 changes: 1 addition & 4 deletions theia/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,4 @@
LOGIN_URL = '/login'
LOGOUT_URL = '/logout'
LOGIN_REDIRECT_URL = 'home'
LOGOUT_REDIRECT_URL = 'home'

SOCIAL_AUTH_PANOPTES_KEY = os.getenv('PANOPTES_PROD_CLIENT_ID')
SOCIAL_AUTH_PANOPTES_SECRET = os.getenv('PANOPTES_PROD_CLIENT_SECRET')
LOGOUT_REDIRECT_URL = 'home'
2 changes: 1 addition & 1 deletion theia/utils/panoptes_oauth2.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def get_user_details(self, response):

ids = ['admin user']
if not user['admin']:
ids = [project.id for project in Project.where()]
ids = [project.href for project in Project.where(current_user_roles='collaborator')]

return {
'username': user['login'],
Expand Down