Skip to content

Commit

Permalink
Merge branch 'release/1.21.0'
Browse files Browse the repository at this point in the history
  • Loading branch information
rajadain committed Nov 9, 2017
2 parents 11d724f + 50481bb commit 87a39df
Show file tree
Hide file tree
Showing 150 changed files with 6,402 additions and 916 deletions.
4 changes: 3 additions & 1 deletion deployment/ansible/group_vars/all
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,16 @@ stack_color: "Black"

itsi_client_id: "model-my-watershed"

client_app_user_password: "mmw"

postgresql_username: mmw
postgresql_password: mmw
postgresql_database: mmw

postgresql_version: "9.4"
postgresql_package_version: "9.4.*.pgdg14.04+1"
postgresql_support_repository_channel: "main"
postgresql_support_libpq_version: "10.0-*.pgdg14.04+1"
postgresql_support_libpq_version: "10.1-*.pgdg14.04+1"
postgresql_support_psycopg2_version: "2.7"
postgis_version: "2.1"
postgis_package_version: "2.1.*.pgdg14.04+1"
Expand Down
2 changes: 1 addition & 1 deletion deployment/ansible/roles.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
- src: azavea.ntp
version: 0.1.1
- src: azavea.pip
version: 0.1.1
version: 1.0.0
- src: azavea.nodejs
version: 0.3.0
- src: azavea.git
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ envdir_config:
MMW_ITSI_CLIENT_ID: "{{ itsi_client_id }}"
MMW_ITSI_SECRET_KEY: "{{ itsi_secret_key }}"
MMW_ITSI_BASE_URL: "{{ itsi_base_url }}"
MMW_CLIENT_APP_USER_PASSWORD: "{{ client_app_user_password }}"
MMW_STACK_COLOR: "{{ stack_color }}"
MMW_TILECACHE_BUCKET: "{{ tilecache_bucket_name }}"
MMW_STACK_TYPE: "{{ stack_type }}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
rwd_data_path: "/opt/rwd-data"
rwd_host: "localhost"
rwd_port: 5000
rwd_docker_image: "quay.io/wikiwatershed/rwd:1.2.2"
rwd_docker_image: "quay.io/wikiwatershed/rwd:1.2.3"

app_config:
RWD_HOST: "{{ rwd_host }}"
Expand Down
12 changes: 11 additions & 1 deletion deployment/cfn/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ class Application(StackNode):
'ITSISecretKey': ['global:ITSISecretKey'],
'RollbarServerSideAccessToken':
['global:RollbarServerSideAccessToken'],
'ClientAppUserPassword': ['global:ClientAppUserPassword'],
}

DEFAULTS = {
Expand Down Expand Up @@ -240,6 +241,11 @@ def set_up_stack(self):
Description='Secret key for ITSI portal integration'
), 'ITSISecretKey')

self.client_app_user_password = self.add_parameter(Parameter(
'ClientAppUserPassword', Type='String', NoEcho=True,
Description='Password for the client apps django account',
), 'ClientAppUserPassword')

app_server_lb_security_group, \
app_server_security_group = self.create_security_groups()
app_server_lb, \
Expand Down Expand Up @@ -582,7 +588,11 @@ def get_cloud_config(self, tile_distribution_endpoint):
' - path: /etc/mmw.d/env/MMW_ITSI_SECRET_KEY\n',
' permissions: 0750\n',
' owner: root:mmw\n',
' content: ', Ref(self.itsi_secret_key)]
' content: ', Ref(self.itsi_secret_key), '\n',
' - path: /etc/mmw.d/env/MMW_CLIENT_APP_USER_PASSWORD\n',
' permissions: 0750\n',
' owner: root:mmw\n',
' content: ', Ref(self.client_app_user_password)]

def create_cloud_watch_resources(self, app_server_lb):
self.add_resource(cw.Alarm(
Expand Down
1 change: 1 addition & 0 deletions deployment/default.yml.example
Original file line number Diff line number Diff line change
Expand Up @@ -64,3 +64,4 @@ WorkerAutoScalingScheduleEndRecurrence: '0 1 * * *'
ITSIBaseURL: ''
ITSISecretKey: ''
RollbarServerSideAccessToken: ''
ClientAppUserPassword: ''
127 changes: 127 additions & 0 deletions doc/arch/adr-005-api-client-app-auth.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
# Geoprocessing API: Only Allow Anonymous Requests From The Client App

## Context

We're creating a [proper, publicly documented API](https://github.com/WikiWatershed/model-my-watershed/blob/develop/src/mmw/apps/geoprocessing_api/views.py) around our analyze and
RWD endpoints: the [Geoprocessing API.](https://github.com/WikiWatershed/model-my-watershed/blob/develop/doc/arch/adr-004-geoprocessing-api.md) The analyze and RWD endpoints have
always had allow-any access; we'd now like to secure them via token authentication
so that we can better track individual users, prevent users from sending off too many
requests, revoke the access of problematic users, and some day maybe add a
paid tier of use.

Enforcing token authentication on these previously open endpoints causes
a problem for our client app; the client app consumes the same API, and
want to continue to allow unauthenticated users access to these endpoints
via the app. We need a way to identify the client app as a special case
from which the app should allow unrestricted requests.

Because everything we send to and from the client is exposed to the app user,
any mechanism we give the client app to identify itself will be fairly
easy to uncover and then fake by anyone trying to get un-checked access to the
API. With this in mind our solution should prioritize not be overly complicated/difficult
for us when we know there's no truly secure solution (barring doing all our rendering
server-side).

Some possibilities are:

1. Give each environment of the client app its own API token
- Keeps a single system for API authentication; easy to reason about;
as a bonus will allow us to cycle the client app's token if someone
decides to start using it

1. Set a special, is-client-app flag that the API would check to determine if authentication was necessary
- To set the flag we'd have to know the request was from the client app in
the first place. Preliminary checks for doing this via `http_referer`
were unsuccessful, which would leave doing this via custom header (or some
other part of the request). Setting a `X-IsClientApp` custom header might
be overly naive or hacky, and could result in a lot of conditional logic

1. Do (1) and also enforce the `HTTP_REFERER` header be from an expected domain.
[Google Maps ](https://developers.google.com/maps/documentation/javascript/get-api-key#key-restrictions)
does this. Even though such headers are easily spoofed by a client outside of a browser,
they're fairly effective for browser-based apps and for naive use from a non-browser client

## Decision

**Give the client app its own API token**

Setting up the client app with its own API token will result a cleaner
architecture for the API. All users use a token, no exceptions. In addition to
the simpler mental model the API token method provides, we'll also gain an added
level of token enforcement; if some API user tries to programmatically use the client
app's token instead of their own, we'll be able to easily cycle the client app's token
to de-incentivize them.

One easy way to cycle the token:
```
./manage.py drf_create_token -r <client_app_username>
```

We can always implement (3) as an enhancement later on.

##### When should the client app send its token

For all Geoprocessing API requests, whether there's a logged-in user's token available
to use or not, ie., we will continue to use a user's credentials when needed for the
application endpoints.


##### How the client app should get the token

If we hard-code the token on the frontend (or anywhere), we'll have to deploy any time we need
to cycle the token. We should instead pass the token to the client app via `clientSettings`.

##### How the server should get the token

To get the token to put in the client's settings, the server can look up the
token from the `authtoken_token` table via the client app's `user_id`.
The `user_id` should remain secret so that there's no programmatic way get
the token outside of swiping it off the client app.

## Consequences

#### Backdoor to the API
As discussed in [Context](#Context), allowing the client app to make requests without a user creates a
backdoor into the API. This is acceptable because up until now we've allowed unrestricted API access
(we just haven't advertised the API as something you could use.) There's also no sensitive data available
via the API. While it's not ideal that someone could use the client app token to make unlimited,
resource-intensive requests unchecked the decision will at least allow us to cycle the token whenever
there's an issue.

#### More Complex Throttling

We want to throttle users of the API, and if the client app is just another, regular user
of the API with a token it would get throttled the same. All guest MMW users in the world
would share a cumalitive number of requests per minute.

To fix this we'll need to treat the client app's token as a special case for throttling:

if token is client app:

don't throttle or cache number of requests per IP
(like [AnonRateThrottle](http://www.django-rest-framework.org/api-guide/throttling/#anonratethrottle)).
There may be difficulties with this related to which IP DRF uses;
it may use that of our load balancer instead of the client's. This also may
cause issues for our classroom users if set too low.

else:

cache number of requests per token (like [UserRateThrottle](http://www.django-rest-framework.org/api-guide/throttling/#userratethrottle))

#### Dev Setup

The `user_id` the app server uses to get the client's token should be fairly
constant for the life of staging and production. Each developer's machine,
however, will have different database instances and, therefore, different
client app `user_id`'s.

We could:
1. Use some special email or name like `clientapp09212017` for the app server to look up the `user_id` to get the token. This would add a level
of indirection, but would allow production, staging and our local machines
to share a migration that creates the user, and share code to look up the id.
There's precedence for this setup at Azavea in Raster Foundry's airflow user.

1. Store the `user_id` itself as an environment variable. A migration could
create the user and write its id to an envfile. We would have to include the envfile in our `.gitignore`, and be careful not to tamper with it.


2 changes: 2 additions & 0 deletions src/mmw/apps/bigcz/clients/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
'model': cuahsi.model,
'serializer': cuahsi.serializer,
'search': cuahsi.search,
'details': cuahsi.details,
'values': cuahsi.values,
'is_pageable': False,
},
}
16 changes: 15 additions & 1 deletion src/mmw/apps/bigcz/clients/cinergi/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,23 @@

class CinergiResource(Resource):
def __init__(self, id, description, author, links, title,
created_at, updated_at, geom, cinergi_url):
created_at, updated_at, geom, cinergi_url,
source_name, contact_organizations, contact_people,
categories, begin_date, end_date,
resource_type, resource_topic_categories,
web_resources, web_services):
super(CinergiResource, self).__init__(id, description, author, links,
title, created_at, updated_at,
geom)

self.cinergi_url = cinergi_url
self.source_name = source_name
self.contact_organizations = contact_organizations
self.contact_people = contact_people
self.categories = categories
self.begin_date = begin_date
self.end_date = end_date
self.resource_type = resource_type
self.resource_topic_categories = resource_topic_categories
self.web_resources = web_resources
self.web_services = web_services
Loading

0 comments on commit 87a39df

Please sign in to comment.