Skip to content

Commit

Permalink
Merge pull request #2248 from WikiWatershed/arr/api-authtoken
Browse files Browse the repository at this point in the history
Geoprocessing API: Add Auth Tokens
  • Loading branch information
Alice Rottersman committed Sep 19, 2017
2 parents 8905503 + f4dfe97 commit 8d3fb99
Show file tree
Hide file tree
Showing 9 changed files with 214 additions and 15 deletions.
26 changes: 26 additions & 0 deletions src/mmw/apps/core/permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from __future__ import print_function
from __future__ import unicode_literals

from rest_framework.permissions import BasePermission
from rest_framework.authentication import TokenAuthentication

from django.conf import settings


class IsTokenAuthenticatedOrClientApp(BasePermission):
"""
Global permission check for request being
(a) from a whitelisted IP
ie. is this our own client web app and not the swagger docs?
(b) from a user authenticated via auth token
"""

def has_permission(self, request, view):
ip_addr = request.META['REMOTE_ADDR']
from_swagger = 'api/docs' in request.META['HTTP_REFERER'] \
if 'HTTP_REFERER' in request.META else False
whitelisted = ip_addr in settings.ALLOWED_HOSTS
token_authenticated = type(request.successful_authenticator) \
is TokenAuthentication
return (not from_swagger and whitelisted) or token_authenticated
4 changes: 4 additions & 0 deletions src/mmw/apps/geoprocessing_api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
from __future__ import division

from django.conf.urls import include, patterns, url
import rest_framework.authtoken.views

from apps.modeling.views import get_job
from apps.modeling.urls import uuid_regex
from apps.geoprocessing_api import views


urlpatterns = patterns(
'',
url(r'^docs/', include('rest_framework_swagger.urls')),
url(r'^api-token-auth/', rest_framework.authtoken.views.obtain_auth_token,
name="authtoken"),
url(r'analyze/land/$', views.start_analyze_land,
name='start_analyze_land'),
url(r'analyze/soil/$', views.start_analyze_soil,
Expand Down
139 changes: 128 additions & 11 deletions src/mmw/apps/geoprocessing_api/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,24 @@

from rest_framework.response import Response
from rest_framework import decorators
from rest_framework.permissions import AllowAny
from rest_framework.authentication import TokenAuthentication

from django.utils.timezone import now
from django.core.urlresolvers import reverse
from django.conf import settings

from apps.core.models import Job
from apps.core.tasks import save_job_error, save_job_result
from apps.core.tasks import (save_job_error,
save_job_result)
from apps.core.permissions import IsTokenAuthenticatedOrClientApp
from apps.modeling import geoprocessing
from apps.modeling.views import load_area_of_interest
from apps.geoprocessing_api import tasks


@decorators.api_view(['POST'])
@decorators.permission_classes((AllowAny, ))
@decorators.authentication_classes((TokenAuthentication, ))
@decorators.permission_classes((IsTokenAuthenticatedOrClientApp, ))
def start_rwd(request, format=None):
"""
Starts a job to run Rapid Watershed Delineation on a point-based location.
Expand Down Expand Up @@ -145,6 +148,11 @@ def start_rwd(request, format=None):
- name: body
required: true
paramType: body
- name: Authorization
paramType: header
description: Format "Token YOUR_API_TOKEN_HERE". When using
Swagger you may wish to set this for all requests via
the field at the top right of the page.
consumes:
- application/json
produces:
Expand Down Expand Up @@ -175,7 +183,8 @@ def start_rwd(request, format=None):


@decorators.api_view(['POST'])
@decorators.permission_classes((AllowAny, ))
@decorators.authentication_classes((TokenAuthentication, ))
@decorators.permission_classes((IsTokenAuthenticatedOrClientApp, ))
def start_analyze_land(request, format=None):
"""
Starts a job to produce a land-use histogram for a given area.
Expand Down Expand Up @@ -344,7 +353,11 @@ def start_analyze_land(request, format=None):
the HUC-12 City of Philadelphia-Schuylkill River.
type: string
paramType: query
- name: Authorization
paramType: header
description: Format "Token YOUR_API_TOKEN_HERE". When using
Swagger you may wish to set this for all requests via
the field at the top right of the page.
consumes:
- application/json
produces:
Expand All @@ -364,7 +377,8 @@ def start_analyze_land(request, format=None):


@decorators.api_view(['POST'])
@decorators.permission_classes((AllowAny, ))
@decorators.authentication_classes((TokenAuthentication, ))
@decorators.permission_classes((IsTokenAuthenticatedOrClientApp, ))
def start_analyze_soil(request, format=None):
"""
Starts a job to produce a soil-type histogram for a given area.
Expand Down Expand Up @@ -465,6 +479,11 @@ def start_analyze_soil(request, format=None):
type: string
paramType: query
- name: Authorization
paramType: header
description: Format "Token YOUR_API_TOKEN_HERE". When using
Swagger you may wish to set this for all requests via
the field at the top right of the page.
consumes:
- application/json
produces:
Expand All @@ -484,7 +503,8 @@ def start_analyze_soil(request, format=None):


@decorators.api_view(['POST'])
@decorators.permission_classes((AllowAny, ))
@decorators.authentication_classes((TokenAuthentication, ))
@decorators.permission_classes((IsTokenAuthenticatedOrClientApp, ))
def start_analyze_animals(request, format=None):
"""
Starts a job to produce counts for animals in a given area.
Expand Down Expand Up @@ -566,10 +586,17 @@ def start_analyze_animals(request, format=None):
paramType: body
type: object
- name: wkaoi
paramType: query
description: The table and ID for a well-known area of interest,
such as a HUC.
Format "table__id", eg. "huc12__55174" will analyze
the HUC-12 City of Philadelphia-Schuylkill River.
- name: Authorization
paramType: header
description: Format "Token YOUR_API_TOKEN_HERE". When using
Swagger you may wish to set this for all requests via
the field at the top right of the page.
consumes:
- application/json
produces:
Expand All @@ -586,7 +613,8 @@ def start_analyze_animals(request, format=None):


@decorators.api_view(['POST'])
@decorators.permission_classes((AllowAny, ))
@decorators.authentication_classes((TokenAuthentication, ))
@decorators.permission_classes((IsTokenAuthenticatedOrClientApp, ))
def start_analyze_pointsource(request, format=None):
"""
Starts a job to analyze the discharge monitoring report annual
Expand Down Expand Up @@ -653,6 +681,11 @@ def start_analyze_pointsource(request, format=None):
such as a HUC.
Format "table__id", eg. "huc12__55174" will analyze
the HUC-12 City of Philadelphia-Schuylkill River.
- name: Authorization
paramType: header
description: Format "Token YOUR_API_TOKEN_HERE". When using
Swagger you may wish to set this for all requests via
the field at the top right of the page.
consumes:
- application/json
produces:
Expand All @@ -669,7 +702,8 @@ def start_analyze_pointsource(request, format=None):


@decorators.api_view(['POST'])
@decorators.permission_classes((AllowAny, ))
@decorators.authentication_classes((TokenAuthentication, ))
@decorators.permission_classes((IsTokenAuthenticatedOrClientApp, ))
def start_analyze_catchment_water_quality(request, format=None):
"""
Starts a job to calculate the calibrated GWLF-E (MapShed) model
Expand Down Expand Up @@ -760,10 +794,16 @@ def start_analyze_catchment_water_quality(request, format=None):
paramType: body
type: object
- name: wkaoi
paramType: query
description: The table and ID for a well-known area of interest,
such as a HUC.
Format "table__id", eg. "huc12__55174" will analyze
the HUC-12 City of Philadelphia-Schuylkill River.
- name: Authorization
paramType: header
description: Format "Token YOUR_API_TOKEN_HERE". When using
Swagger you may wish to set this for all requests via
the field at the top right of the page.
consumes:
- application/json
produces:
Expand All @@ -780,10 +820,87 @@ def start_analyze_catchment_water_quality(request, format=None):


@decorators.api_view(['POST'])
@decorators.permission_classes((AllowAny, ))
@decorators.authentication_classes((TokenAuthentication, ))
@decorators.permission_classes((IsTokenAuthenticatedOrClientApp, ))
def start_analyze_climate(request, format=None):
"""
Start Climate Analyze job
Start a job to calculate the monthly climate (precipitation
and mean temperature) of a given area.
Source PRISM Climate Group
For more information, see
the [technical documentation](https://wikiwatershed.org/
documentation/mmw-tech/#overlays-tab-coverage)
## Response
You can use the url provided in the response's `location`
header to poll for the job's results.
<summary>
**Example of a completed job's `result`**
</summary>
<details>
{
"survey": {
"displayName": "Climate",
"name": "climate",
"categories": [
{
"ppt": 66.84088134765625,
"tmean": -2.4587886333465576,
"monthidx": 1,
"month": "January"
},
{
"ppt": 59.17946434020996,
"tmean": -1.8310737609863281,
"monthidx": 2,
"month": "February"
}, ...
]
}
}
</details>
---
type:
job:
required: true
type: string
status:
required: true
type: string
omit_serializer: true
parameters:
- name: body
description: A valid single-ringed Multipolygon GeoJSON
representation of the shape to analyze.
See the GeoJSON spec
https://tools.ietf.org/html/rfc7946#section-3.1.7
paramType: body
type: object
- name: wkaoi
paramType: query
description: The table and ID for a well-known area of interest,
such as a HUC.
Format "table__id", eg. "huc12__55174" will analyze
the HUC-12 City of Philadelphia-Schuylkill River.
- name: Authorization
paramType: header
description: Format "Token&nbsp;YOUR_API_TOKEN_HERE". When using
Swagger you may wish to set this for all requests via
the field at the top right of the page.
consumes:
- application/json
produces:
- application/json
"""
user = request.user if request.user.is_authenticated() else None

Expand Down
14 changes: 12 additions & 2 deletions src/mmw/apps/modeling/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
from rest_framework.response import Response
from rest_framework import decorators, status
from rest_framework.exceptions import ParseError
from rest_framework.authentication import (SessionAuthentication,
TokenAuthentication)
from rest_framework.permissions import (AllowAny,
IsAuthenticated,
IsAuthenticatedOrReadOnly)
Expand All @@ -25,6 +27,7 @@

from apps.core.models import Job
from apps.core.tasks import save_job_error, save_job_result
from apps.core.permissions import IsTokenAuthenticatedOrClientApp
from apps.modeling import tasks, geoprocessing
from apps.modeling.mapshed.tasks import (geoprocessing_chains,
combine,
Expand Down Expand Up @@ -482,7 +485,9 @@ def drb_point_sources(request):


@decorators.api_view(['GET'])
@decorators.permission_classes((AllowAny, ))
@decorators.authentication_classes((TokenAuthentication,
SessionAuthentication, ))
@decorators.permission_classes((IsTokenAuthenticatedOrClientApp, ))
def get_job(request, job_uuid, format=None):
"""
Get a job's status. If it's complete, get its result.
Expand All @@ -509,6 +514,12 @@ def get_job(request, job_uuid, format=None):
type: string
omit_serializer: true
parameters:
- name: Authorization
paramType: header
description: Format "Token&nbsp;YOUR_API_TOKEN_HERE". When using
Swagger you may wish to set this for all requests via
the field at the top right of the page.
"""
# TODO consider if we should have some sort of session id check to ensure
# you can only view your own jobs.
Expand All @@ -520,7 +531,6 @@ def get_job(request, job_uuid, format=None):
# Get the user so that logged in users can only see jobs that they started
# or anonymous ones
user = request.user if request.user.is_authenticated() else None

if job.user and job.user != user:
raise Http404("Not found.")

Expand Down
24 changes: 24 additions & 0 deletions src/mmw/apps/user/migrations/0002_auth_tokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations
from django.conf import settings
from django.contrib.auth.models import User
from rest_framework.authtoken.models import Token


def add_auth_tokens_to_users(apps, schema_editor):
for user in User.objects.all():
Token.objects.create(user=user)


class Migration(migrations.Migration):

dependencies = [
('authtoken', '0001_initial'),
('user', '0001_initial')
]

operations = [
migrations.RunPython(add_auth_tokens_to_users)
]
15 changes: 15 additions & 0 deletions src/mmw/apps/user/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
# -*- coding: utf-8 -*-

from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver

from rest_framework.authtoken.models import Token


@receiver(post_save, sender=settings.AUTH_USER_MODEL)
def create_auth_token(sender, instance=None, created=False, **kwargs):
"""
Create an auth token for every newly created user.
"""
if created:
Token.objects.create(user=instance)


class ItsiUserManager(models.Manager):
Expand Down
Loading

0 comments on commit 8d3fb99

Please sign in to comment.