Skip to content

Commit

Permalink
Feature/issue 186 Implement API keys (#188)
Browse files Browse the repository at this point in the history
* API Gateway Lambda authorizer to facilitate API keys and usage plans

* Unit tests to test Lambda authorizer

* Fix terraform file formatting

* API Gateway Lambda Authorizer

- Lambda function
- API Keys and Authorizer definition in OpenAPI spec
- API gateway API keys
- API gateway usage plans
- SSM parameters for API keys

* Fix trailing whitespace

* Set default region environment variable

* Fix SNYK vulnerabilities

* Add issue to changelog

* Implement custom trusted partner header x-hydrocron-key

* Update cryptography for SNYK vulnerability

* Update documentation to include API key usage

* Update quota and throttle settings for API Gateway

* Update API keys documentation to indicate to be implemented

* Move API key lookup to Lambda INIT

* Remove API key authentication and update API key to x-hydrocron-key
  • Loading branch information
nikki-t authored Jun 18, 2024
1 parent 3ff1733 commit 5d86e7a
Show file tree
Hide file tree
Showing 19 changed files with 663 additions and 114 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- Issue 186 - Implement API keys to control usage
- Issue 183 - Update documentation examples and provide a brief intro to the timeseries endpoint
- Issue 100 - Add option to 'compact' GeoJSON result into single feature
- Issue 101 - Add support for HTTP Accept header
Expand Down
48 changes: 48 additions & 0 deletions docs/timeseries.md
Original file line number Diff line number Diff line change
Expand Up @@ -443,3 +443,51 @@ Example CSV response:
| 415 | 'Unsupported Media Type': The user send an invalid `Accept` header. |

*The 400 code is also currently returned for queries where no time series data could be located for the request specified feature ID. The message returned with the response indicates this and it can be helpful to adjust the date ranges you are searching.

## API Keys [DRAFT]

> ⚠️
>API keys not yet implemented but coming soon! Content below is not finalized. More details to follow...
Users may request a special API key for cases where their intended usage of the API may be considered heavy or more complex. Heavy usage can be defined as continued used with over x requests per day or continue use which require many requests per second or concurrent requests. To request an API key or to discuss your use case, please contact us at x.

**Note: Users do *not* have to send an API key in their request to use the Hydrocron API. The API key is optional.**

### How to use an API key in requests [DRAFT]

Hydrocron API key header: `x-hydrocron-key`

After receiving the API key, users may send the API key in their request under the `x-hydrocron-key` header.

Example

```bash
curl --header 'x-hydrocron-key: <podaac-provided-api-key>' --location 'https://soto.podaac.earthdatacloud.nasa.gov/hydrocron/v1/timeseries?feature=Reach&feature_id=63470800171&start_time=2024-02-01T00:00:00%2b00:00&end_time=2024-10-30T00:00:00%2b00:00&fields=reach_id,time_str,wse'
```

Replace `<podaac-provided-api-key>` with the API key provided to you.

Python Example

```python
import requests

url = "https://soto.podaac.earthdatacloud.nasa.gov/hydrocron/v1/timeseries"

headers = {
"x-hydrocon-key": "<podaac-provided-api-key>"
}

params = {
"feature": "Reach",
"feature_id": "63470800171",
"output": "csv",
"start_time": "2024-02-01T00:00:00%2b00:00",
"end_time": "2024-10-30T00:00:00%2b00:00",
"fields": "reach_id,time_str,wse,slope,width"
}

response = requests.get(url=url, headers=headers, params=params)
```

Replace `<podaac-provided-api-key>` with the API key provided to you.
60 changes: 60 additions & 0 deletions hydrocron/api/controllers/authorizer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""
Lambda Authorizer to facilitate usage of API keys and usage plans.
Taken from example:
https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-use-lambda-authorizer.html
"""

import json
import logging

from hydrocron.utils import connection


logging.getLogger().setLevel(logging.INFO)


ssm_client = connection.ssm_client
STORED_API_KEY_TRUSTED = ssm_client.get_parameter(Name="/service/hydrocron/api-key-trusted", WithDecryption=True)["Parameter"]["Value"]
STORED_API_KEY_DEFAULT = ssm_client.get_parameter(Name="/service/hydrocron/api-key-default", WithDecryption=True)["Parameter"]["Value"]


def authorization_handler(event, context):
"""Lambda authorizer function to allow or deny a request."""

logging.info("Event: %s", event)
logging.info("Context: %s", context)

api_key_trusted = "" if "x-hydrocron-key" not in event["headers"].keys() else event["headers"]["x-hydrocron-key"]

if api_key_trusted and api_key_trusted == STORED_API_KEY_TRUSTED:
response_policy = create_policy("trusted_partner", "Allow", event["methodArn"], STORED_API_KEY_TRUSTED)
logging.info("Created policy for truster partner.")

else:
response_policy = create_policy("default_user", "Allow", event["methodArn"], STORED_API_KEY_DEFAULT)
logging.info("Created policy for default user.")

logging.info("Response: %s", response_policy)
return json.loads(response_policy)


def create_policy(principle_id, effect, method_arn, api_key=""):
"""Create IAM policy to return in authorizer response."""

authorization_response = {
"principalId": principle_id,
"policyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Action": "execute-api:Invoke",
"Effect": effect,
"Resource": method_arn,
}
]
},
"usageIdentifierKey": api_key
}

return json.dumps(authorization_response)
14 changes: 14 additions & 0 deletions hydrocron/utils/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import boto3
from boto3.resources.base import ServiceResource
import botocore
from botocore.client import BaseClient


class Connection(ModuleType):
Expand All @@ -27,6 +28,7 @@ def __init__(self, name):
self._dynamodb_resource = None
self._dynamodb_endpoint = self._get_dynamodb_endpoint()
self._s3_resource = None
self._ssm_client = None

def _get_dynamodb_endpoint(self):
"""Return dynamodb endpoint URL."""
Expand Down Expand Up @@ -69,8 +71,20 @@ def s3_resource(self) -> ServiceResource:

return self._s3_resource

@property
def ssm_client(self) -> BaseClient:
"""Return SSM client."""

if not self._ssm_client:

ssm_session = boto3.session.Session()
self._ssm_client = ssm_session.client('ssm')

return self._ssm_client


dynamodb_resource: ServiceResource
s3_resource: ServiceResource
ssm_client: BaseClient

sys.modules[__name__] = Connection(__name__)
Loading

0 comments on commit 5d86e7a

Please sign in to comment.