Skip to content

Commit

Permalink
Merge pull request #166 from ASFHyP3/develop
Browse files Browse the repository at this point in the history
Release: add browse and thumbnail images and GET /user
  • Loading branch information
Jlrine2 authored Jul 14, 2020
2 parents b1a5eca + 48309ad commit 0caa329
Show file tree
Hide file tree
Showing 13 changed files with 243 additions and 56 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
with:
query: .github/queries/asssociated-pr.query.yml
outputFile: pr.json
owner: asfadmin
owner: ASFHyP3
name: hyp3
sha: ${{ github.sha }}

Expand All @@ -99,7 +99,7 @@ jobs:
with:
query: .github/queries/pr-labels.query.yml
outputFile: labels.json
owner: asfadmin
owner: ASFHyP3
name: hyp3

- name: Tag version
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ jobs:
PRE_RELEASE: "false"
ALLOW_EMPTY_CHANGELOG: "false"
ALLOW_TAG_PREFIX: "true"
RELEASE_NAME_PREFIX: HYp3-
RELEASE_NAME_PREFIX: HyP3-
10 changes: 9 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0] - 2020-07-14
### Added
- New GET /user API endpoint to query job quota and jobs remaining
- browse image key for each job containing a list of urls for browse images
- browse images expire at the same time as products
- thumbnail image key for each job containing a list of urls for thumbnail images
- thubnail images expire at the same time as products

## [0.1.0] - 2020-06-08
### Added
- API checks granule exists in CMR and rejects jobs with granules that are not found
- API checks granule exists in CMR and rejects jobs with granules that are not found
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# hyp3
![Static code analysis](https://github.com/asfadmin/hyp3/workflows/Static%20code%20analysis/badge.svg)
![Deploy to AWS](https://github.com/asfadmin/hyp3/workflows/Deploy%20to%20AWS/badge.svg)
![Static code analysis](https://github.com/ASFHyP3/hyp3/workflows/Static%20code%20analysis/badge.svg)
![Deploy to AWS](https://github.com/ASFHyP3/hyp3/workflows/Deploy%20to%20AWS/badge.svg)
31 changes: 25 additions & 6 deletions api/src/hyp3_api/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from connexion.apps.flask_app import FlaskJSONEncoder
from flask_cors import CORS
from hyp3_api import DYNAMODB_RESOURCE, connexion_app
from hyp3_api.util import CmrError, QuotaError, check_granules_exist, check_quota_for_user, format_time,\
from hyp3_api.util import CmrError, check_granules_exist, format_time, get_remaining_jobs_for_user,\
get_request_time_expression


Expand All @@ -27,10 +27,10 @@ def post_jobs(body, user):
if not context['is_authorized']:
return problem(403, 'Forbidden', f'User {user} does not have permission to submit jobs.')

try:
check_quota_for_user(user, len(body['jobs']))
except QuotaError as e:
return problem(400, 'Bad Request', str(e))
quota = get_user(user)['quota']
if quota['remaining'] - len(body['jobs']) < 0:
message = 'Your monthly quota is {quota["limit"]} jobs. You have {quota["remaining"]} jobs remaining.'
return problem(400, 'Bad Request', message)

try:
granules = [job['job_parameters']['granule'] for job in body['jobs']]
Expand Down Expand Up @@ -69,10 +69,29 @@ def get_jobs(user, start=None, end=None, status_code=None):
KeyConditionExpression=key_expression,
FilterExpression=filter_expression,
)

return {'jobs': response['Items']}


def get_user(user):
authorized = context['is_authorized']

if authorized:
limit = int(environ['MONTHLY_JOB_QUOTA_PER_USER'])
remaining = get_remaining_jobs_for_user(user)
else:
limit = 0
remaining = 0

return {
'user_id': user,
'authorized': authorized,
'quota': {
'limit': limit,
'remaining': remaining,
},
}


connexion_app.app.json_encoder = DecimalEncoder
connexion_app.add_api('openapi-spec.yml', validate_responses=True, strict_validation=True)
CORS(connexion_app.app, origins=r'https?://([-\w]+\.)*asf\.alaska\.edu', supports_credentials=True)
46 changes: 46 additions & 0 deletions api/src/hyp3_api/openapi-spec.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ paths:
application/json:
schema:
$ref: "#/components/schemas/jobs_response"
/user:
get:
operationId: hyp3_api.handlers.get_user
responses:
"200":
description: 200 response
content:
application/json:
schema:
$ref: "#/components/schemas/user"

components:
schemas:
Expand All @@ -71,6 +81,33 @@ components:
jobs:
$ref: "#/components/schemas/list_of_jobs"

user:
type: object
required:
- user_id
- authorized
- quota
properties:
user_id:
$ref: "#/components/schemas/user_id"
authorized:
type: boolean
quota:
$ref: "#/components/schemas/quota"

quota:
type: object
required:
- limit
- remaining
properties:
limit:
type: integer
minimum: 0
remaining:
type: integer
minimum: 0

list_of_new_jobs:
type: array
minItems: 1
Expand Down Expand Up @@ -124,6 +161,10 @@ components:
$ref: "#/components/schemas/description"
files:
$ref: "#/components/schemas/list_of_files"
browse_images:
$ref: "#/components/schemas/list_of_urls"
thumbnail_images:
$ref: "#/components/schemas/list_of_urls"
expiration_time:
$ref: "#/components/schemas/datetime"

Expand Down Expand Up @@ -196,6 +237,11 @@ components:
url:
type: string

list_of_urls:
type: array
items:
type: string

securitySchemes:
EarthDataLogin:
description: |-
Expand Down
7 changes: 3 additions & 4 deletions api/src/hyp3_api/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,11 @@ def format_time(time: datetime):
return utc_time.isoformat(timespec='seconds')


def check_quota_for_user(user, number_of_jobs):
def get_remaining_jobs_for_user(user):
previous_jobs = get_job_count_for_month(user)
quota = int(environ['MONTHLY_JOB_QUOTA_PER_USER'])
job_count = previous_jobs + number_of_jobs
if job_count > quota:
raise QuotaError(f'Your monthly quota is {quota} jobs. You have {quota - previous_jobs} jobs remaining.')
remaining_jobs = quota - previous_jobs
return max(remaining_jobs, 0)


def get_job_count_for_month(user):
Expand Down
9 changes: 8 additions & 1 deletion api/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

AUTH_COOKIE = 'asf-urs'
JOBS_URI = '/jobs'
USER_URI = '/user'

DEFAULT_JOB_ID = 'myJobId'
DEFAULT_USERNAME = 'test_username'
Expand Down Expand Up @@ -71,7 +72,9 @@ def make_db_record(job_id,
request_time='2019-12-31T15:00:00+00:00',
status_code='RUNNING',
expiration_time='2019-12-31T15:00:00+00:00',
files=None):
files=None,
browse_images=None,
thumbnail_images=None):
record = {
'job_id': job_id,
'user_id': user_id,
Expand All @@ -84,6 +87,10 @@ def make_db_record(job_id,
}
if files is not None:
record['files'] = files
if browse_images is not None:
record['browse_images'] = browse_images
if thumbnail_images is not None:
record['thumbnail_images'] = thumbnail_images
if expiration_time is not None:
record['expiration_time'] = expiration_time
return record
Expand Down
79 changes: 48 additions & 31 deletions api/tests/test_api_spec.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
from conftest import AUTH_COOKIE, DEFAULT_USERNAME, JOBS_URI, login, make_job, submit_batch
from conftest import AUTH_COOKIE, DEFAULT_USERNAME, JOBS_URI, USER_URI, login, make_job, submit_batch
from flask_api import status
from hyp3_api import auth


ENDPOINTS = {
JOBS_URI: {'GET', 'HEAD', 'OPTIONS', 'POST'},
USER_URI: {'GET', 'HEAD', 'OPTIONS'},
}


def test_options(client):
response = client.options(JOBS_URI)
assert response.status_code == status.HTTP_200_OK
allowed_methods = response.headers['allow'].split(', ')
assert sorted(allowed_methods) == ['GET', 'HEAD', 'OPTIONS', 'POST']
all_methods = {'GET', 'HEAD', 'OPTIONS', 'POST', 'PUT', 'DELETE'}

login(client)
for uri, good_methods in ENDPOINTS.items():
response = client.options(uri)
assert response.status_code == status.HTTP_200_OK
allowed_methods = response.headers['allow'].split(', ')
assert set(allowed_methods) == good_methods

for bad_method in all_methods - good_methods:
response = client.open(uri, method=bad_method)
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED


def test_not_logged_in(client):
for method in ['POST', 'GET', 'HEAD']:
response = client.open(JOBS_URI, method=method)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
for uri, methods in ENDPOINTS.items():
for method in methods:
response = client.open(uri, method=method)
if method == 'OPTIONS':
assert response.status_code == status.HTTP_200_OK
else:
assert response.status_code == status.HTTP_401_UNAUTHORIZED


def test_logged_in_not_authorized(client):
Expand All @@ -24,15 +42,17 @@ def test_logged_in_not_authorized(client):


def test_invalid_cookie(client):
client.set_cookie('localhost', AUTH_COOKIE, 'garbage I say!!! GARGBAGE!!!')
response = submit_batch(client)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
for uri in ENDPOINTS:
client.set_cookie('localhost', AUTH_COOKIE, 'garbage I say!!! GARGBAGE!!!')
response = client.get(uri)
assert response.status_code == status.HTTP_401_UNAUTHORIZED


def test_expired_cookie(client):
client.set_cookie('localhost', AUTH_COOKIE, auth.get_mock_jwt_cookie('user', -1))
response = submit_batch(client)
assert response.status_code == status.HTTP_401_UNAUTHORIZED
for uri in ENDPOINTS:
client.set_cookie('localhost', AUTH_COOKIE, auth.get_mock_jwt_cookie('user', -1))
response = client.get(uri)
assert response.status_code == status.HTTP_401_UNAUTHORIZED


def test_good_granule_names(client, table):
Expand Down Expand Up @@ -94,32 +114,28 @@ def test_bad_product_types(client):
assert response.status_code == status.HTTP_400_BAD_REQUEST


def test_jobs_bad_method(client):
for method in ['PUT', 'DELETE']:
response = client.open(JOBS_URI, method=method)
assert response.status_code == status.HTTP_405_METHOD_NOT_ALLOWED


def test_no_route(client):
response = client.get('/no/such/path')
assert response.status_code == status.HTTP_404_NOT_FOUND


def test_cors_no_origin(client):
response = client.post(JOBS_URI)
assert 'Access-Control-Allow-Origin' not in response.headers
assert 'Access-Control-Allow-Credentials' not in response.headers
for uri in ENDPOINTS:
response = client.get(uri)
assert 'Access-Control-Allow-Origin' not in response.headers
assert 'Access-Control-Allow-Credentials' not in response.headers


def test_cors_bad_origins(client):
bad_origins = [
'https://www.google.com',
'https://www.alaska.edu',
]
for origin in bad_origins:
response = client.post(JOBS_URI, headers={'Origin': origin})
assert 'Access-Control-Allow-Origin' not in response.headers
assert 'Access-Control-Allow-Credentials' not in response.headers
for uri in ENDPOINTS:
for origin in bad_origins:
response = client.get(uri, headers={'Origin': origin})
assert 'Access-Control-Allow-Origin' not in response.headers
assert 'Access-Control-Allow-Credentials' not in response.headers


def test_cors_good_origins(client):
Expand All @@ -128,7 +144,8 @@ def test_cors_good_origins(client):
'https://search-test.asf.alaska.edu',
'http://local.asf.alaska.edu',
]
for origin in good_origins:
response = client.post(JOBS_URI, headers={'Origin': origin})
assert response.headers['Access-Control-Allow-Origin'] == origin
assert response.headers['Access-Control-Allow-Credentials'] == 'true'
for uri in ENDPOINTS:
for origin in good_origins:
response = client.get(uri, headers={'Origin': origin})
assert response.headers['Access-Control-Allow-Origin'] == origin
assert response.headers['Access-Control-Allow-Credentials'] == 'true'
Loading

0 comments on commit 0caa329

Please sign in to comment.